[
  {
    "path": ".circleci/config.yml",
    "content": "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    1.18.4-erlang-27.3.3-alpine-3.21.3,\n    1.17.3-erlang-27.1.3-alpine-3.20.3,\n    1.16.1-erlang-26.2.2-alpine-3.19.1,\n    1.15.7-erlang-26.2.2-alpine-3.19.1,\n    1.14.5-erlang-25.3.2.9-alpine-3.19.1,\n    1.13.4-erlang-24.3.4-alpine-3.15.3,\n    1.12.3-erlang-24.3.4-alpine-3.15.3,\n    1.11.4-erlang-23.3.4.13-alpine-3.15.3,\n  ]\n\njobs:\n  check-license:\n    docker:\n      - image: fsfe/reuse:latest\n    steps:\n      - checkout\n      - run: reuse lint\n\n  build-test:\n    parameters:\n      tag:\n        type: string\n    docker:\n      - image: hexpm/elixir:<< parameters.tag >>\n    working_directory: ~/repo\n    environment:\n      LC_ALL: C.UTF-8\n    steps:\n      - run:\n          name: Install system dependencies\n          command: apk add --no-cache build-base procps\n      - checkout\n      - run:\n          name: Install hex and rebar\n          command: |\n            mix local.hex --force\n            mix local.rebar --force\n      - restore_cache:\n          keys:\n            - v1-mix-cache-<< parameters.tag >>-{{ checksum \"mix.lock\" }}\n      - run: mix deps.get\n      - run: mix test --exclude cgroup\n      - when:\n          condition:\n            matches: { <<: *latest, value: << parameters.tag >> }\n          steps:\n            - run: mix format --check-formatted\n            - run: mix deps.unlock --check-unused\n            - run: mix docs\n            - run: mix hex.build\n            - run: mix credo -a --strict\n            - run: mix dialyzer\n      - save_cache:\n          key: v1-mix-cache-<< parameters.tag >>-{{ checksum \"mix.lock\" }}\n          paths:\n            - _build\n            - deps\n\nworkflows:\n  checks:\n    jobs:\n      - check-license\n      - build-test:\n          name: << matrix.tag >>\n          matrix:\n            parameters:\n              tag: *tags\n"
  },
  {
    "path": ".credo.exs",
    "content": "# .credo.exs\n%{\n  configs: [\n    %{\n      name: \"default\",\n      strict: true,\n      checks: [\n        {Credo.Check.Refactor.MapInto, false},\n        {Credo.Check.Warning.LazyLogging, false},\n        {Credo.Check.Readability.LargeNumbers, only_greater_than: 86400},\n        {Credo.Check.Readability.ParenthesesOnZeroArityDefs, parens: true},\n        {Credo.Check.Readability.Specs, tags: []},\n        {Credo.Check.Readability.StrictModuleLayout, tags: []}\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": ".formatter.exs",
    "content": "# Used by \"mix format\"\n[\n  inputs: [\"*.{ex,exs}\", \"{config,lib,test}/**/*.{ex,exs}\"]\n]\n"
  },
  {
    "path": ".gitignore",
    "content": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up here.\n/cover/\n\n# The directory Mix downloads your dependencies sources to.\n/deps/\n\n# Where 3rd-party dependencies like ExDoc output generated docs.\n/doc/\n\n# Ignore .fetch files in case you like to edit your project deps locally.\n/.fetch\n\n# If the VM crashes, it generates a dump, let's ignore it too.\nerl_crash.dump\n\n# Also ignore archive artifacts (built via \"mix archive.build\").\n*.ez\n\n# Ignore package tarball (built via \"mix hex.build\").\nmuontrap-*.tar\n\n/priv\n\n*.o\n/muontrap-*.log\ntest/*.test\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## v1.7.0\n\n* New feature\n  * Add `:capture_stderr_only` option to capture only stderr while ignoring stdout.\n    This is useful when you want to capture error messages but not regular output.\n    Works with both `MuonTrap.cmd/3` and `MuonTrap.Daemon`. (@fermuch)\n\n## v1.6.1\n\n* Bug fixes\n  * Ignore transient EAGAIN, EWOULDBLOCK, and EINTR errors when processing\n    acknowledgments from Erlang. These would cause unneeded restarts.\n    (@mediremi)\n\n## v1.6.0\n\n* New feature\n  * Add `:logger_fun` option to `MuonTrap.Daemon` to allow complete\n    customization of the logging process. Pass it a 1-arity function or `mfargs`\n    tuple. This option takes precedence over all of the other log related\n    options.  (@bjyoungblood)\n\n## v1.5.0\n\n* New feature\n  * Add Logger metadata in `MuonTrap.Daemon`. See the `:logger_metadata` option.\n    (@bjyoungblood)\n\n## v1.4.1\n\n* Bug fixes\n  * Support logging output to all Elixir logger levels. Previously the \"new\" set\n    that includes emergency, critical, warning, etc. would fail the option check\n  * Default the `log_transform` option to replace invalid UTF8 characters so\n    they don't crash the Logger. This fixes an annoyance where a program would\n    do this and there'd be log crash spam. It's still overridable, so users\n    using custom loggers that already handle this can pass\n    `Function.identity/1` to disable. (@jjcarstens)\n\n## v1.4.0\n\n* New feature\n  * Add a timeout option to `MuonTrap.cmd/3`. OS processes that take too long\n    will be killed and a `:timeout` return status returned. This is backwards\n    compatible. Thanks to @bjyoungblood for adding this feature.\n\n## v1.3.3\n\n* Bug fixes\n  * Fix issue where lots of prints from a child process when the Erlang process\n    side is killed can cause MuonTrap to not clean up the child process. There\n    are some potential variations on this that were also fixed even though they\n    were unseen. Thanks to @bjyoungblood for figuring this out.\n\n* Improvements\n  * Improve debug logging so that when enabled, fatal errors are written to the\n    log as well and not to stderr.\n\n## v1.3.2\n\n* Bug fixes\n  * Fix C compiler error when building with older versions of gcc. This fixes an\n    compile error with Ubuntu 20.04, for example.\n\n## v1.3.1\n\n* Bug fixes\n  * Fix regression where stderr would be printed when `stderr_to_stdout: true`\n    was specified and logging disabled.\n\n## v1.3.0\n\n* New feature\n  * Add flow control to stdout (and stderr if capturing it) to prevent\n    out-of-memory VM crashes from programs that can spam stdout. The output\n    would accumulate in the process mailbox waiting to be processed. The flow\n    control implementation will push back and slow down output generation. The\n    number of bytes in flight defaults to 10 KB and is set with the new\n    `:stdio_window` parameter. (@jjcarstens)\n\n* Bug fixes\n  * Fix various minor issues preventing unit tests from passing on MacOS.\n    (@jjcarstens)\n\n## v1.2.0\n\n* New feature\n  * Added `:exit_status_to_reason` to the `Daemon` to be able to change how the\n    `Daemon` GenServer exits based on the exit status of the program being run.\n    (@erauer)\n\n## v1.1.0\n\n* New features\n  * Support transforming output from programs before sending to the log. See the\n    new `:log_transform` option. (@brunoro)\n\n## v1.0.0\n\nThis release only changes the version number. It has no code changes.\n\n## v0.6.1\n\nThis release has no code changes.\n\n* Improvements\n  * Clean up build prints, fix a doc typo, and update dependencies for fresher\n    docs.\n\n## v0.6.0\n\n* Bug fixes\n  * Fix the `:delay_to_sigkill` option so that it takes milliseconds as\n    documented and remove the max delay check. Previously, the code used\n    microseconds for the delay despite the documentation. If you were using\n    `:delay_to_sigkill`, this is a backwards incompatible change and your delays\n    will be 1000x longer. Thanks to Almir for reporting this issue.\n\n## v0.5.1\n\n* New features\n  * Added the `:log_prefix` option to MuonTrap.Daemon so that logged output can\n    be annotated in more helpful ways. This is useful when running the same\n    program multiple times, but with different configurations.\n\n## v0.5.0\n\nThis update contains many changes throughout. If you're using cgroups, please\nreview the changes as they likely affect your code.\n\n* New features\n  * Added `:cgroup_base`. The preferred way of using cgroups now is for MuonTrap\n    to create a sub-cgroup for running the command. This removes the need to\n    keep track of cgroup paths on your own when you run more than one command at\n    a time. `:cgroup_path` is still available.\n  * Almost all inconsistencies between MuonTrap.Daemon and MuonTrap.cmd/3 have\n    been fixed. As a result, MuonTrap.Daemon detects and raises more exceptions\n    than previous. It is possible that code that worked before will now break.\n  * MuonTrap.Daemon sets its exit status based on the process's exit code.\n    Successful exit codes (exit code 0) exit `:normal` and failed exit codes\n    (anything else) do not. This makes it possible to use the Supervisor\n    `:temporary` restart strategy that only restarts failures.\n  * MuonTrap.Daemon supports a `:name` parameter for setting GenServer names.\n  * MuonTrap.Daemon `cgget` and `cgset` helpers return ok/error tuples now since\n    it was too easy to accidentally call them such that they'd raise.\n\n* Bug fixes\n  * Forcefully killed processes would get stuck in a zombie state until the kill\n    timeout expired due to a missing call to wait(2). This has been fixed.\n  * Exit status of process killed by a signal reflects that. I.e., a process\n    killed by a signal exits with a status of 128+signal.\n\n## v0.4.4\n\n* Bug fixes\n  * Fixed an issue where environment variable lists passed to MuonTrap.Daemon\n    had to be charlists rather than Elixir strings like MuonTrap.cmd/3 and\n    System.cmd/3.\n\n## v0.4.3\n\n* Bug fixes\n  * Reverted removal of `child_spec`\n\n## v0.4.2\n\n* New features\n  * MuonTrap.Daemon can log stderr now as well as stdout. Pass\n    `stderr_to_stdout: true` in the options. Thanks to Timmo Verlaan for this\n    update.\n\n## v0.4.1\n\n* Improvements\n  * Move port process build products under `_build`. This fixes an issue where\n    changes in MIX_TARGET settings would not be picked up.\n  * Improved some specs to remove Dialyzer warnings in some cases\n\n## v0.4.0\n\n* New features\n  * MuonTrap.Daemon no longer sends all of the output from the process to the\n    logger by default. If you want it logged, pass in a `{:log_output, level}`\n    option. This also slightly improves the logged message to make it easier\n    to read.\n\n## v0.3.1\n\n* Bug fixes\n  * Make MuonTrap.Daemon usable (child_specs, options)\n\n## v0.3.0\n\n* Bug fixes\n  * Make MuonTrap.cmd/3 pass the System.cmd/3 tests\n  * Add a few more specs and fix Dialyzer errors\n\n## v0.2.2\n\n* Bug fixes\n  * Add missing dependency on `:logger`\n\n## v0.2.1\n\n* Bug fixes\n  * Fix hex package contents\n\n## v0.2.0\n\n* Bug fixes\n  * Fix shutdown timeout and issues with getting EINTR\n  * More progress on cgroup testing; docs\n\n## v0.1.0\n\n* Initial release\n"
  },
  {
    "path": "LICENSES/Apache-2.0.txt",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.\n\n\"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.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising permissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.\n\n\"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.\n\n\"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).\n\n\"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.\n\n\"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.\"\n\n\"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.\n\n2. 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.\n\n3. 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.\n\n4. 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:\n\n     (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and\n\n     (b) You must cause any modified files to carry prominent notices stating that You changed the files; and\n\n     (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\n\n     (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.\n\n     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.\n\n5. 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.\n\n6. 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.\n\n7. 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.\n\n8. 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.\n\n9. 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.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\nTo 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.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "LICENSES/CC-BY-4.0.txt",
    "content": "Creative Commons Attribution 4.0 International\n\n 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.\n\nUsing Creative Commons Public Licenses\n\nCreative 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.\n\nConsiderations 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.\n\nConsiderations 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.\n\nCreative Commons Attribution 4.0 International Public License\n\nBy 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.\n\nSection 1 – Definitions.\n\n     a.\tAdapted 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.\n\n     b.\tAdapter'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.\n\n     c.\tCopyright 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.\n\n     d.\tEffective 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.\n\n     e.\tExceptions 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.\n\n     f.\tLicensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.\n\n     g.\tLicensed 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.\n\n     h.\tLicensor means the individual(s) or entity(ies) granting rights under this Public License.\n\n     i.\tShare 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.\n\n     j.\tSui 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.\n\n     k.\tYou means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.\n\nSection 2 – Scope.\n\n     a.\tLicense grant.\n\n          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:\n\n               A. reproduce and Share the Licensed Material, in whole or in part; and\n\n               B. produce, reproduce, and Share Adapted Material.\n\n          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.\n\n          3. Term. The term of this Public License is specified in Section 6(a).\n\n          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.\n\n          5. Downstream recipients.\n\n               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.\n\n               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.\n\n          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).\n\nb. Other rights.\n\n          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.\n\n          2. Patent and trademark rights are not licensed under this Public License.\n\n          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.\n\nSection 3 – License Conditions.\n\nYour exercise of the Licensed Rights is expressly made subject to the following conditions.\n\n     a.\tAttribution.\n\n          1. If You Share the Licensed Material (including in modified form), You must:\n\n               A. retain the following if it is supplied by the Licensor with the Licensed Material:\n\n                    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);\n\n                    ii. a copyright notice;\n\n                    iii. a notice that refers to this Public License;\n\n                    iv.\ta notice that refers to the disclaimer of warranties;\n\n                    v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;\n\n               B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and\n\n               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.\n\n          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.\n\n          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.\n\n          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.\n\nSection 4 – Sui Generis Database Rights.\n\nWhere the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:\n\n     a.\tfor 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;\n\n     b.\tif 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\n\n     c.\tYou must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.\nFor 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.\n\nSection 5 – Disclaimer of Warranties and Limitation of Liability.\n\n     a.\tUnless 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.\n\n     b.\tTo 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.\n\n     c.\tThe 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.\n\nSection 6 – Term and Termination.\n\n     a.\tThis 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.\n\n     b.\tWhere Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:\n\n          1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or\n\n          2. upon express reinstatement by the Licensor.\n\n     c.\tFor 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.\n\n     d.\tFor 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.\n\n     e.\tSections 1, 5, 6, 7, and 8 survive termination of this Public License.\n\nSection 7 – Other Terms and Conditions.\n\n     a.\tThe Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.\n\n     b.\tAny 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.\n\nSection 8 – Interpretation.\n\n     a.\tFor 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.\n\n     b.\tTo 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.\n\n     c.\tNo term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.\n\n     d.\tNothing 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.\n\nCreative 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.\n\nCreative Commons may be contacted at creativecommons.org.\n"
  },
  {
    "path": "LICENSES/CC0-1.0.txt",
    "content": "Creative Commons Legal Code\n\nCC0 1.0 Universal\n\n    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN\n    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS\n    INFORMATION ON AN \"AS-IS\" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES\n    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS\n    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM\n    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED\n    HEREUNDER.\n\nStatement of Purpose\n\nThe laws of most jurisdictions throughout the world automatically confer\nexclusive Copyright and Related Rights (defined below) upon the creator\nand subsequent owner(s) (each and all, an \"owner\") of an original work of\nauthorship and/or a database (each, a \"Work\").\n\nCertain owners wish to permanently relinquish those rights to a Work for\nthe purpose of contributing to a commons of creative, cultural and\nscientific works (\"Commons\") that the public can reliably and without fear\nof later claims of infringement build upon, modify, incorporate in other\nworks, reuse and redistribute as freely as possible in any form whatsoever\nand for any purposes, including without limitation commercial purposes.\nThese owners may contribute to the Commons to promote the ideal of a free\nculture and the further production of creative, cultural and scientific\nworks, or to gain reputation or greater distribution for their Work in\npart through the use and efforts of others.\n\nFor these and/or other purposes and motivations, and without any\nexpectation of additional consideration or compensation, the person\nassociating CC0 with a Work (the \"Affirmer\"), to the extent that he or she\nis an owner of Copyright and Related Rights in the Work, voluntarily\nelects to apply CC0 to the Work and publicly distribute the Work under its\nterms, with knowledge of his or her Copyright and Related Rights in the\nWork and the meaning and intended legal effect of CC0 on those rights.\n\n1. Copyright and Related Rights. A Work made available under CC0 may be\nprotected by copyright and related or neighboring rights (\"Copyright and\nRelated Rights\"). Copyright and Related Rights include, but are not\nlimited to, the following:\n\n  i. the right to reproduce, adapt, distribute, perform, display,\n     communicate, and translate a Work;\n ii. moral rights retained by the original author(s) and/or performer(s);\niii. publicity and privacy rights pertaining to a person's image or\n     likeness depicted in a Work;\n iv. rights protecting against unfair competition in regards to a Work,\n     subject to the limitations in paragraph 4(a), below;\n  v. rights protecting the extraction, dissemination, use and reuse of data\n     in a Work;\n vi. database rights (such as those arising under Directive 96/9/EC of the\n     European Parliament and of the Council of 11 March 1996 on the legal\n     protection of databases, and under any national implementation\n     thereof, including any amended or successor version of such\n     directive); and\nvii. other similar, equivalent or corresponding rights throughout the\n     world based on applicable law or treaty, and any national\n     implementations thereof.\n\n2. Waiver. To the greatest extent permitted by, but not in contravention\nof, applicable law, Affirmer hereby overtly, fully, permanently,\nirrevocably and unconditionally waives, abandons, and surrenders all of\nAffirmer's Copyright and Related Rights and associated claims and causes\nof action, whether now known or unknown (including existing as well as\nfuture claims and causes of action), in the Work (i) in all territories\nworldwide, (ii) for the maximum duration provided by applicable law or\ntreaty (including future time extensions), (iii) in any current or future\nmedium and for any number of copies, and (iv) for any purpose whatsoever,\nincluding without limitation commercial, advertising or promotional\npurposes (the \"Waiver\"). Affirmer makes the Waiver for the benefit of each\nmember of the public at large and to the detriment of Affirmer's heirs and\nsuccessors, fully intending that such Waiver shall not be subject to\nrevocation, rescission, cancellation, termination, or any other legal or\nequitable action to disrupt the quiet enjoyment of the Work by the public\nas contemplated by Affirmer's express Statement of Purpose.\n\n3. Public License Fallback. Should any part of the Waiver for any reason\nbe judged legally invalid or ineffective under applicable law, then the\nWaiver shall be preserved to the maximum extent permitted taking into\naccount Affirmer's express Statement of Purpose. In addition, to the\nextent the Waiver is so judged Affirmer hereby grants to each affected\nperson a royalty-free, non transferable, non sublicensable, non exclusive,\nirrevocable and unconditional license to exercise Affirmer's Copyright and\nRelated Rights in the Work (i) in all territories worldwide, (ii) for the\nmaximum duration provided by applicable law or treaty (including future\ntime extensions), (iii) in any current or future medium and for any number\nof copies, and (iv) for any purpose whatsoever, including without\nlimitation commercial, advertising or promotional purposes (the\n\"License\"). The License shall be deemed effective as of the date CC0 was\napplied by Affirmer to the Work. Should any part of the License for any\nreason be judged legally invalid or ineffective under applicable law, such\npartial invalidity or ineffectiveness shall not invalidate the remainder\nof the License, and in such case Affirmer hereby affirms that he or she\nwill not (i) exercise any of his or her remaining Copyright and Related\nRights in the Work or (ii) assert any associated claims and causes of\naction with respect to the Work, in either case contrary to Affirmer's\nexpress Statement of Purpose.\n\n4. Limitations and Disclaimers.\n\n a. No trademark or patent rights held by Affirmer are waived, abandoned,\n    surrendered, licensed or otherwise affected by this document.\n b. Affirmer offers the Work as-is and makes no representations or\n    warranties of any kind concerning the Work, express, implied,\n    statutory or otherwise, including without limitation warranties of\n    title, merchantability, fitness for a particular purpose, non\n    infringement, or the absence of latent or other defects, accuracy, or\n    the present or absence of errors, whether or not discoverable, all to\n    the greatest extent permissible under applicable law.\n c. Affirmer disclaims responsibility for clearing rights of other persons\n    that may apply to the Work or any use thereof, including without\n    limitation any person's Copyright and Related Rights in the Work.\n    Further, Affirmer disclaims responsibility for obtaining any necessary\n    consents, permissions or other rights required for any use of the\n    Work.\n d. Affirmer understands and acknowledges that Creative Commons is not a\n    party to this document and has no duty or obligation with respect to\n    this CC0 or use of the Work.\n"
  },
  {
    "path": "Makefile",
    "content": "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$(MAKE) -C c_src clean\n\tif [ -f test/Makefile ]; then $(MAKE) -C test clean; fi\n\n.PHONY: all clean calling_from_make\n\n.SILENT:\n"
  },
  {
    "path": "NOTICE",
    "content": "Muontrap is open-source software licensed under the Apache License, Version\n2.0.\n\nCopyright holders include Frank Hunleth, Matt Ludwigs, Jason Axelson, Timmo\nVerlaan, Aldebaran Alonso, Gustavo Brunoro, Ben Youngblood, Eric Rauer, Jon\nCarstens, Milan Vit, Fernando Mumbach and Médi-Rémi Hashim.\n\nAuthoritative REUSE-compliant copyright and license metadata available at\nhttps://hex.pm/packages/muontrap.\n"
  },
  {
    "path": "README.md",
    "content": "# MuonTrap\n\n[![Hex version](https://img.shields.io/hexpm/v/muontrap.svg \"Hex version\")](https://hex.pm/packages/muontrap)\n[![API docs](https://img.shields.io/hexpm/v/muontrap.svg?label=hexdocs \"API docs\")](https://hexdocs.pm/muontrap/MuonTrap.html)\n[![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)\n[![REUSE status](https://api.reuse.software/badge/github.com/fhunleth/muontrap)](https://api.reuse.software/info/github.com/fhunleth/muontrap)\n\nKeep programs, daemons, and applications launched from Erlang and Elixir\ncontained and well-behaved. This lightweight library kills OS processes if the\nElixir process running them crashes and if you're running on Linux, it can use\ncgroups to prevent many other shenanigans.\n\nSome other features:\n\n* Attach your OS process to a supervision tree via a convenient `child_spec`\n* Set `cgroup` controls like thresholds on memory and CPU utilization\n* Start OS processes as a different user or group\n* Send SIGKILL to processes that aren't responsive to SIGTERM\n* With `cgroups`, ensure that all children of launched processes have been killed too\n\n## TL;DR\n\nAdd `muontrap` to your project's `mix.exs` dependency list:\n\n```elixir\ndef deps do\n  [\n    {:muontrap, \"~> 1.0\"}\n  ]\nend\n```\n\nRun a command similar to\n[`System.cmd/3`](https://hexdocs.pm/elixir/System.html#cmd/3):\n\n```elixir\niex>  MuonTrap.cmd(\"echo\", [\"hello\"])\n{\"hello\\n\", 0}\n```\n\nAttach a long running process to a supervision tree using a\n[child_spec](https://hexdocs.pm/elixir/Supervisor.html#module-child-specification)\nlike the following:\n\n```elixir\n{MuonTrap.Daemon, [\"long_running_command\", [\"arg1\", \"arg2\"], options]}\n```\n\nRunning on Linux and can use cgroups? Then create a new cgroup:\n\n```bash\nsudo cgcreate -a $(whoami) -g memory:mycgroup\n```\n\n```elixir\n{MuonTrap.Daemon,\n [\n   \"long_running_command\",\n   [\"arg1\", \"arg2\"],\n   [cgroup_controllers: [\"memory\"], cgroup_base: \"mycgroup\"]\n ]}\n```\n\n`MuonTrap` will create a cgroup under \"mycgroup\" to run the\n`\"long_running_command\"`. If the command fails, it will be restarted. If it\nshould no longer be running (like if something else crashed in Elixir and\nsupervision needs to clean up) then MuonTrap will kill `\"long_running_command\"`\nand all of its children.\n\nWant to know more about the motivations for this library? Read on in the\n[Background](#background) section.\n\n## FAQ\n\n### How do I watch stdout?\n\nIf you're using `MuonTrap.cmd/3`, you don't get the called program's output\nuntil after it exits. Just like `System.cmd/3`, the `:into` option can be used\nto get the output as it's printed. Here's an example.\n\n```elixir\nMuonTrap.cmd(\"my_program\", [], stderr_to_stdout: true, into: IO.binstream(:stdio, :line))\n```\n\nIf you're using `MuonTrap.Daemon`, then the best way is to send output to the\nlogger. There are quite a few options, so see the `MuonTrap.Daemon` docs on what\nmakes sense for you.\n\n### How do I stop a MuonTrap.Daemon?\n\nTreat the `MuonTrap.Daemon` process just like any other Elixir process. If you\nput it in a supervision tree, call `Supervisor.terminate_child/2`. If you have\nit's pid, call `Process.exit/2`.\n\n## Background\n\nThe Erlang VM's port interface lets Elixir applications run external programs.\nThis is important since it's not practical to rewrite everything in Elixir.\nPlus, if the program is long running like a daemon or a server, you use Elixir\nto supervise it and restart it on crashes. The catch is that the Erlang VM\nexpects port processes to be well-behaved. As you'd expect, many useful programs\ndon't quite meet the Erlang VM's expectations.\n\nFor example, let's say that you want to monitor a network connection and decide\nthat `ping` is the right tool. Here's how you could start `ping` in a process.\n\n```elixir\niex> pid = spawn(fn -> System.cmd(\"ping\", [\"-i\", \"5\", \"localhost\"], into: IO.stream(:stdio, :line)) end)\n#PID<0.6116.0>\nPING localhost (127.0.0.1): 56 data bytes\n64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.032 ms\n64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.077 ms\n```\n\nTo see that `ping` is running, call `ps` to look for it. You can also do this\nfrom a separate terminal window outside of IEx:\n\n```elixir\niex> :os.cmd('ps -ef | grep ping') |> IO.puts\n  501 38820 38587   0  9:26PM ??         0:00.01 /sbin/ping -i 5 localhost\n  501 38824 38822   0  9:27PM ??         0:00.00 grep ping\n:ok\n```\n\nNow exit the Elixir process. Imagine here that in the real program that\nsomething happened in Elixir and the process needs to exit and be restarted by a\nsupervisor.\n\n```elixir\niex> Process.exit(pid, :oops)\ntrue\niex> :os.cmd('ps -ef | grep ping') |> IO.puts\n  501 38820 38587   0  9:26PM ??         0:00.02 /sbin/ping -i 5 localhost\n  501 38833 38831   0  9:34PM ??         0:00.00 grep ping\n```\n\nAs you can tell, `ping` is still running after the exit. If you run `:observer`\nyou'll see that Elixir did indeed terminate both the process and the port, but\nthat didn't stop `ping`. The reason for this is that `ping` doesn't pay\nattention to `stdin` and doesn't notice the Erlang VM closing it to signal that\nit should exit.\n\nImagine now that the process was supervised and it restarts. If this happens a\nregularly, you could be running dozens of `ping` commands.\n\nThis is just one of the problems that `muontrap` fixes.\n\n## Applicability\n\nThis is intended for long running processes. It's not great for interactive\nprograms that communicate via the port or send signals. That feature is possible\nto add, but you'll probably be happier with other solutions like\n[erlexec](https://github.com/saleyn/erlexec/).\n\n## Running commands\n\nThe simplest way to use `muontrap` is as a replacement to `System.cmd/3`. Here's\nan example using `ping`:\n\n```elixir\niex> pid = spawn(fn -> MuonTrap.cmd(\"ping\", [\"-i\", \"5\", \"localhost\"], into: IO.stream(:stdio, :line)) end)\n#PID<0.30860.0>\nPING localhost (127.0.0.1): 56 data bytes\n64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.027 ms\n64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.081 ms\n```\n\nNow if you exit that process, `ping` gets killed as well:\n\n```elixir\niex> Process.exit(pid, :oops)\ntrue\niex> :os.cmd('ps -ef | grep ping') |> IO.puts\n  501 38898 38896   0  9:58PM ??         0:00.00 grep ping\n\n:ok\n```\n\n## Containment with cgroups\n\nEven if you don't make use of any cgroup controller features, having your port\nprocess contained can be useful just to make sure that everything is cleaned\nup on exit including any subprocesses.\n\nTo set this up, first create a cgroup with appropriate permissions. Any path\nwill do; `muontrap` just needs to be able to create a subdirectory underneath it\nfor its use. For example:\n\n```bash\nsudo cgcreate -a $(whoami) -g memory,cpu:mycgroup\n```\n\nBe sure to create the group for all of the cgroup controllers that you wish to\nuse with `muontrap`. The above example creates it for the `memory` and `cpu`\ncontrollers.\n\nIn Elixir, call `MuonTrap.cmd/3` with the\ncgroup options now. In this case, we'll use the `cpu` controller, but this\nexample would work fine with any of the controllers.\n\n```elixir\niex>  MuonTrap.cmd(\"spawning_program\", [], cgroup_controllers: [\"cpu\"], cgroup_base: \"mycgroup\")\n{\"hello\\n\", 0}\n```\n\nIn this example, `muontrap` runs `spawning_program` in a sub-cgroup under the\n`cpu/mycgroup` group. The cgroup parameters may be modified outside of\n`muontrap` using `cgset` or my accessing the cgroup mountpoint manually.\n\nOn any error or if the Erlang VM closes the port or if `spawning_program` exits,\n`muontrap` will kill all OS processes in cgroup. No need to worry about\nrandom processes accumulating on your system.\n\nNote that if you use `cgroup_base`, a temporary cgroup is created for running\nthe command. If you want `muontrap` to use a particular cgroup and not create a\nsubgroup for the command, use the `:cgroup_path` option. Note that if you\nexplicitly specify a cgroup, be careful not to use it for anything else.\n`MuonTrap` assumes that it owns the cgroup and when it needs to kill processes,\nit kills all of them in the cgroup.\n\n### Limit the memory used by a process\n\nLinux's cgroups are very powerful and the examples here only scratch the\nsurface. If you'd like to limit an OS process and all of its child processes to\na maximum amount of memory, you can do that with the `memory` controller:\n\n```elixir\niex>  MuonTrap.cmd(\"memory_hog\", [], cgroup_controllers: [\"memory\"], cgroup_base: \"mycgroup\", cgroup_sets: [{\"memory\", \"memory.limit_in_bytes\", \"268435456\"}])\n```\n\nThat line restricts the total memory used by `memory_hog` to 256 MB.\n\n### Limit CPU usage in a port\n\nLimiting the maximum CPU usage is also possible. Two parameters control that\nwith the `cpu` controller: `cpu.cfs_period_us` specifies the number of\nmicroseconds in the scheduling period and `cpu.cfs_quota_us` specifies how many\nof those microseconds can be used. Here's an example call that prevents a\nprogram from using more than 50% of the CPU:\n\n```elixir\niex>  MuonTrap.cmd(\"cpu_hog\", [], cgroup_controllers: [\"cpu\"], cgroup_base: \"mycgroup\", cgroup_sets: [{\"cpu\", \"cpu.cfs_period_us\", \"100000\"}, {\"cpu\", \"cpu.cfs_quota_us\", 50000}])\n```\n\n## Supervision\n\nFor many long running programs, you may want to restart them if they crash.\nLuckily Erlang already has mechanisms to do this. `MuonTrap` provides a\n`GenServer` called `MuonTrap.Daemon` that you can hook into one of your\nsupervision trees.  For example, you could specify it like this in your\napplication's supervisor:\n\n```elixir\n  def start(_type, _args) do\n    children = [\n      {MuonTrap.Daemon, [\"command\", [\"arg1\", \"arg2\"], options]}\n    ]\n\n    opts = [strategy: :one_for_one, name: MyApp.Supervisor]\n    Supervisor.start_link(children, opts)\n  end\n```\n\nSupervisors provide three restart strategies, `:permanent`, `:temporary`, and\n`:transient`. They work as follows:\n\n* `:permanent` - Always restart the command if it exits or crashes. Restarts are\n  limited to the Supervisor's restart intensity settings as they would be with\n  normal `GenServer`s. This is the default.\n* `:transient` - If the exit status of the command is 0 (i.e., success), then\n  don't restart. Any other exit status is considered an error and the command is\n  restarted.\n* `:temporary` - Don't restart\n\nIf you're running more than one `MuonTrap.Daemon` under the same `Supervisor`,\nthen you'll need to give each one a unique `:id`. Here's an example `child_spec`\nfor setting the `:id` and the `:restart` parameters:\n\n```elixir\n    Supervisor.child_spec(\n        {MuonTrap.Daemon, [\"command\", [\"arg1\"], options]},\n         id: :my_daemon,\n         restart: :transient\n      )\n```\n\n## stdio flow control\n\nThe Erlang port feature does not implement flow control from messages coming\nfrom the port process. Since `MuonTrap` captures stdio from the program being\nrun, it's possible that the program sends output so fast that it grows the\nElixir process's mailbox big enough to cause an out-of-memory error.\n\n`MuonTrap` protects against this by implementing a flow control mechanism. When\ntriggered, the running program's stdout and stderr file handles won't be read\nand hence it will eventually be blocked from writing to those handles.\n\nThe `:stdio_window` option specifies the maximum number of unacknowledged bytes\nallowed. The default is 10 KB.\n\n## muontrap development\n\nIn order to run the tests, some additional tools need to be installed.\nSpecifically the `cgcreate` and `cgget` binaries need to be installed (and\navailable on `$PATH`). Typically the package may be called `cgroup-tools` (on\narch linux you need to install the `libcgroup` aur package).\n\nThen run:\n\n```sh\nsudo cgcreate -a $(whoami) -g memory,cpu:muontrap_test\n```\n\n## License\n\nAll original source code in this project is licensed under Apache-2.0.\n\nAdditionally, this project follows the [REUSE recommendations](https://reuse.software)\nand labels so that licensing and copyright are clear at the file level.\n\nExceptions to Apache-2.0 licensing are:\n\n* Configuration and data files are licensed under CC0-1.0\n* Documentation is CC-BY-4.0\n"
  },
  {
    "path": "REUSE.toml",
    "content": "version = 1\n\n[[annotations]]\npath = [\n \".circleci/config.yml\",\n \".credo.exs\",\n \".formatter.exs\",\n \".github/dependabot.yml\",\n \".gitignore\",\n \"CHANGELOG.md\",\n \"Makefile\",\n \"c_src/Makefile\",\n \"test/Makefile\",\n \"NOTICE\",\n \"REUSE.toml\",\n \"mix.exs\",\n \"mix.lock\"\n]\nprecedence = \"aggregate\"\nSPDX-FileCopyrightText = \"None\"\nSPDX-License-Identifier = \"CC0-1.0\"\n\n[[annotations]]\npath = [\n \"README.md\"\n]\nprecedence = \"aggregate\"\nSPDX-FileCopyrightText = \"2018 Frank Hunleth\"\nSPDX-License-Identifier = \"CC-BY-4.0\"\n"
  },
  {
    "path": "c_src/Makefile",
    "content": "# Makefile for building the muontrap port process\n#\n# Makefile targets:\n#\n# all/install   build and install\n# clean         clean build products and intermediates\n#\n# Variables to override:\n#\n# MIX_APP_PATH  path to the build directory\n# CC            C compiler. MUST be set if crosscompiling\n# CFLAGS        compiler flags for compiling all C files\n# LDFLAGS       linker flags for linking all binaries\n\nPREFIX = $(MIX_APP_PATH)/priv\nBUILD  = $(MIX_APP_PATH)/obj\n\nMUONTRAP = $(PREFIX)/muontrap\n\nLDFLAGS +=\nCFLAGS ?= -O2 -Wall -Wextra -Wno-unused-parameter\n# _GNU_SOURCE is needed for splice(2) on Linux\nCFLAGS += -std=c99 -D_GNU_SOURCE -Wno-empty-body\n\n#CFLAGS += -DDEBUG\n\nSRC = $(wildcard *.c)\nOBJ = $(SRC:%.c=$(BUILD)/%.o)\n\ncalling_from_make:\n\tcd .. && mix compile\n\nall: install\n\ninstall: $(PREFIX) $(BUILD) $(MUONTRAP)\n\n$(OBJ): Makefile\n\n$(BUILD)/%.o: %.c\n\t@echo \" CC $(notdir $@)\"\n\t$(CC) -c $(CFLAGS) -o $@ $<\n\n$(MUONTRAP): $(OBJ)\n\t@echo \" LD $(notdir $@)\"\n\t$(CC) $^ $(LDFLAGS) -o $@\n\n$(PREFIX) $(BUILD):\n\tmkdir -p $@\n\nclean:\n\t$(RM) $(MUONTRAP) $(BUILD)/*.o\n\n.PHONY: all clean calling_from_make install\n\n# Don't echo commands unless the caller exports \"V=1\"\n${V}.SILENT:\n"
  },
  {
    "path": "c_src/muontrap.c",
    "content": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n// SPDX-FileCopyrightText: 2023 Jon Carstens\n// SPDX-FileCopyrightText: 2025 Fernando Mumbach\n// SPDX-FileCopyrightText: 2025 Médi-Rémi Hashim\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <errno.h>\n#include <fcntl.h>\n#include <getopt.h>\n#include <grp.h>\n#include <poll.h>\n#include <pwd.h>\n#include <signal.h>\n#include <stdlib.h>\n#include <stdio.h>\n#include <stdint.h>\n#include <string.h>\n#include <sys/stat.h>\n#include <sys/types.h>\n#include <sys/wait.h>\n#include <time.h>\n#include <unistd.h>\n\n// IMPORTANT:\n// The FATAL* macros mirror err(3) and errx(3) which also exit. Exiting does not clean up\n// the child process which defeats one of the reasons to use MuonTrap in the first place.\n// Be careful to use these macros in places where the child is not running.\n#ifdef DEBUG\nstatic FILE *debug_fp = NULL;\n#define INFO(MSG, ...) do { fprintf(debug_fp, \"%d INFO:\" MSG \"\\n\", microsecs(), ## __VA_ARGS__); fflush(debug_fp); } while (0)\n#define WARN(MSG, ...) do { fprintf(debug_fp, \"%d WARN:\" MSG \"\\n\", microsecs(), ## __VA_ARGS__); fflush(debug_fp); } while (0)\n#define WARNX(MSG, ...) do { fprintf(debug_fp, \"%d WARN:\" MSG \"\\n\", microsecs(), ## __VA_ARGS__); fflush(debug_fp); } while (0)\n#define FATAL(MSG, ...) do { fprintf(debug_fp, \"%d  ERR:\" MSG \"\\n\", microsecs(), ## __VA_ARGS__); fflush(debug_fp); exit(EXIT_FAILURE); } while (0)\n#define FATALX(MSG, ...) do { fprintf(debug_fp, \"%d  ERR:\" MSG \"\\n\", microsecs(), ## __VA_ARGS__); fflush(debug_fp); exit(EXIT_FAILURE); } while (0)\n#else\n#define INFO(MSG, ...) ;\n#define WARN(MSG, ...) ;\n#define WARNX(MSG, ...) ;\n#define FATAL(MSG, ...) do { fprintf(stderr, \"MUONTRAP: \" MSG \"\\n\",  ## __VA_ARGS__); exit(EXIT_FAILURE); } while (0)\n#define FATALX(MSG, ...) do { fprintf(stderr, \"MUONTRAP: \" MSG \"\\n\",  ## __VA_ARGS__); exit(EXIT_FAILURE); } while (0)\n#endif\n\n// asprintf can fail, but it's so rare that it's annoying to see the checks in the code.\n#define checked_asprintf(MSG, ...) do { if (asprintf(MSG, ## __VA_ARGS__) < 0) FATAL(\"asprintf\"); } while (0)\n\nstatic struct option long_options[] = {\n    {\"arg0\", required_argument, 0, '0'},\n    {\"controller\", required_argument, 0, 'c'},\n    {\"help\",     no_argument,       0, 'h'},\n    {\"delay-to-sigkill\", required_argument, 0, 'k'},\n    {\"group\", required_argument, 0, 'g'},\n    {\"set\", required_argument, 0, 's'},\n    {\"uid\", required_argument, 0, 'u'},\n    {\"gid\", required_argument, 0, 'a'},\n    {\"stdio-window\", required_argument, 0, 'l'},\n    {\"capture-output\", no_argument, 0, 'o'},\n    {\"capture-stderr\", no_argument, 0, 'e'},\n    {\"capture-stderr-only\", no_argument, 0, 'r'},\n    {0,          0,                 0, 0 }\n};\n\n#define CGROUP_MOUNT_PATH \"/sys/fs/cgroup\"\n\nstruct controller_var {\n    struct controller_var *next;\n    const char *key;\n    const char *value;\n};\n\nstruct controller_info {\n    const char *name;\n    char *group_path;\n    char *procfile;\n\n    struct controller_var *vars;\n    struct controller_info *next;\n};\n\nstatic struct controller_info *controllers = NULL;\nstatic const char *cgroup_path = NULL;\nstatic int brutal_kill_wait_ms = 500;\nstatic uid_t run_as_uid = 0; // 0 means don't set, since we don't support privilege escalation\nstatic gid_t run_as_gid = 0; // 0 means don't set, since we don't support privilege escalation\n\nstatic int signal_pipe[2] = { -1, -1};\nstatic int stdout_pipe[2] = { -1, -1};\nstatic int stderr_pipe[2] = { -1, -1};\n\n#define DEFAULT_STDIO_WINDOW 10240 // Allow up to 10 KB out to Elixir at a time\nstatic int stdio_bytes_max = DEFAULT_STDIO_WINDOW;\nstatic int stdio_bytes_avail = DEFAULT_STDIO_WINDOW;\nstatic int capture_output = 0; // Don't capture output by default\nstatic int capture_stderr = 0; // If capturing output, don't capture stderr by default\nstatic int capture_stderr_only = 0; // Capture stderr only, ignore stdout\n\n#define FOREACH_CONTROLLER for (struct controller_info *controller = controllers; controller != NULL; controller = controller->next)\n\nstatic void move_pid_to_cgroups(pid_t pid);\n\nstatic void usage()\n{\n    printf(\"Usage: muontrap [OPTION] -- <program> <args>\\n\");\n    printf(\"\\n\");\n    printf(\"Options:\\n\");\n\n    printf(\"--arg0,-0 <arg0>\\n\");\n    printf(\"--controller,-c <cgroup controller> (may be specified multiple times)\\n\");\n    printf(\"--group,-g <cgroup path>\\n\");\n    printf(\"--set,-s <cgroup variable>=<value>\\n (may be specified multiple times)\\n\");\n    printf(\"--delay-to-sigkill,-k <milliseconds>\\n\");\n    printf(\"--stdio-window <bytes>\\n\");\n    printf(\"--capture-output\\n\");\n    printf(\"--capture-stderr\\n\");\n    printf(\"--capture-stderr-only\\n\");\n    printf(\"--uid <uid/user> drop privilege to this uid or user\\n\");\n    printf(\"--gid <gid/group> drop privilege to this gid or group\\n\");\n    printf(\"-- the program to run and its arguments come after this\\n\");\n}\n\nstatic int microsecs()\n{\n    struct timespec ts;\n    clock_gettime(CLOCK_MONOTONIC, &ts);\n    return (ts.tv_sec * 1000000) + (ts.tv_nsec / 1000);\n}\n\nvoid sigchild_handler(int signum)\n{\n    if (signal_pipe[1] >= 0 &&\n            write(signal_pipe[1], &signum, sizeof(signum)) < 0)\n        WARN(\"write(signal_pipe)\");\n}\n\nvoid enable_signal_handlers()\n{\n    struct sigaction sa;\n    sa.sa_handler = sigchild_handler;\n    sigemptyset(&sa.sa_mask);\n    sa.sa_flags = 0;\n\n    sigaction(SIGCHLD, &sa, NULL);\n    sigaction(SIGINT, &sa, NULL);\n    sigaction(SIGQUIT, &sa, NULL);\n    sigaction(SIGTERM, &sa, NULL);\n}\n\nvoid disable_signal_handlers()\n{\n    sigaction(SIGCHLD, NULL, NULL);\n    sigaction(SIGINT, NULL, NULL);\n    sigaction(SIGQUIT, NULL, NULL);\n    sigaction(SIGTERM, NULL, NULL);\n}\n\nstatic int fork_exec(const char *path, char *const *argv)\n{\n    INFO(\"Running %s\", path);\n    for (char *const *arg = argv; *arg != NULL; arg++) {\n        INFO(\"  arg: %s\", *arg);\n    }\n\n    pid_t pid = fork();\n    if (pid == 0) {\n        // child\n\n        // Move to the container\n        move_pid_to_cgroups(getpid());\n\n        if (capture_stderr_only) {\n            // Capture stderr only, send stdout to /dev/null\n            int dev_null_fd = open(\"/dev/null\", O_WRONLY);\n            if (dev_null_fd < 0)\n                FATAL(\"Can't open /dev/null\");\n\n            // Send stdout to /dev/null\n            if (dup2(dev_null_fd, STDOUT_FILENO) < 0)\n                FATAL(\"dup2 STDOUT_FILENO\");\n\n            // Capture stderr\n            if (dup2(stderr_pipe[1], STDERR_FILENO) < 0)\n                FATAL(\"dup2 STDERR_FILENO\");\n\n            close(dev_null_fd);\n        } else if (capture_output) {\n            // Replace stdout a with flow controlled versions\n            if (dup2(stdout_pipe[1], STDOUT_FILENO) < 0)\n                FATAL(\"dup2 STDOUT_FILENO\");\n\n            // If capturing stderr too, do the same thing.\n            if (capture_stderr) {\n                if (dup2(stderr_pipe[1], STDERR_FILENO) < 0)\n                    FATAL(\"dup2 STDERR_FILENO\");\n            }\n        } else {\n            // Not capturing stdout, so send it to /dev/null to get it dropped with as little processing as possible\n            int dev_null_fd = open(\"/dev/null\", O_WRONLY);\n            if (dev_null_fd < 0)\n                FATAL(\"Can't open /dev/null\");\n\n            if (dup2(dev_null_fd, STDOUT_FILENO) < 0)\n                FATAL(\"dup2 STDOUT_FILENO\");\n\n            // If not capturing output at all, but the user says to capture\n            // stderr, send stderr to /dev/null as well. As odd as this sounds\n            // here, it's due to the `:stderr_to_stdout` option mapping to\n            // `capture_stderr`.\n            if (capture_stderr) {\n                if (dup2(dev_null_fd, STDERR_FILENO) < 0)\n                    FATAL(\"dup2 STDERR_FILENO\");\n            }\n\n            close(dev_null_fd);\n        }\n\n        // Drop/change privilege if requested\n        // See https://wiki.sei.cmu.edu/confluence/display/c/POS36-C.+Observe+correct+revocation+order+while+relinquishing+privileges\n        if (run_as_gid > 0 && setgid(run_as_gid) < 0)\n            FATAL(\"setgid(%d)\", run_as_gid);\n\n        if (run_as_uid > 0 && setuid(run_as_uid) < 0)\n            FATAL(\"setuid(%d)\", run_as_uid);\n\n        execvp(path, argv);\n\n        // Not supposed to reach here.\n        exit(EXIT_FAILURE);\n    } else {\n\n        return pid;\n    }\n}\n\nstatic int mkdir_p(const char *abspath, int start_index)\n{\n    int rc = 0;\n    int last_errno = 0;\n    char *group_path = strdup(abspath);\n    for (int i = start_index; ; i++) {\n        if (group_path[i] == '/' || group_path[i] == 0) {\n            char save = group_path[i];\n            group_path[i] = 0;\n            rc = mkdir(group_path, 0755);\n            if (rc < 0)\n                last_errno = errno;\n\n            group_path[i] = save;\n            if (save == 0)\n                break;\n        }\n    }\n    free(group_path);\n\n    // Return the last call to mkdir since that's the one that matters\n    // and earlier directories are likely already created.\n    errno = last_errno;\n    return rc;\n}\n\nstatic void create_cgroups()\n{\n    FOREACH_CONTROLLER {\n        int start_index = strlen(CGROUP_MOUNT_PATH) + 1 + strlen(controller->name) + 1;\n        INFO(\"Create cgroup: mkdir -p %s\", controller->group_path);\n        if (mkdir_p(controller->group_path, start_index) < 0) {\n            if (errno == EEXIST)\n                FATALX(\"'%s' already exists. Please specify a deeper group_path or clean up the cgroup\",\n                     controller->group_path);\n            else\n                FATAL(\"Couldn't create '%s'. Check permissions.\", controller->group_path);\n        }\n    }\n}\n\nstatic int write_file(const char *group_path, const char *value)\n{\n   FILE *fp = fopen(group_path, \"w\");\n   if (!fp)\n       return -1;\n\n   int rc = fwrite(value, 1, strlen(value), fp);\n   fclose(fp);\n   return rc;\n}\n\nstatic void update_cgroup_settings()\n{\n    FOREACH_CONTROLLER {\n        for (struct controller_var *var = controller->vars;\n             var != NULL;\n             var = var->next) {\n            char *setting_file;\n            checked_asprintf(&setting_file, \"%s/%s\", controller->group_path, var->key);\n            if (write_file(setting_file, var->value) < 0)\n                FATAL(\"Error writing '%s' to '%s'\", var->value, setting_file);\n            free(setting_file);\n        }\n    }\n}\n\nstatic void move_pid_to_cgroups(pid_t pid)\n{\n    FOREACH_CONTROLLER {\n        FILE *fp = fopen(controller->procfile, \"w\");\n        if (fp == NULL ||\n            fprintf(fp, \"%d\", pid) < 0)\n            FATAL(\"Can't add pid to %s\", controller->procfile);\n        fclose(fp);\n    }\n}\n\nstatic void destroy_cgroups()\n{\n    FOREACH_CONTROLLER {\n        // Only remove the final directory, since we don't keep track of\n        // what we actually create.\n        INFO(\"rmdir %s\", controller->group_path);\n        if (rmdir(controller->group_path) < 0) {\n            INFO(\"Error removing %s (%s)\", controller->group_path, strerror(errno));\n            WARN(\"Error removing %s\", controller->group_path);\n        }\n    }\n}\n\nstatic int procfile_killall(const char *group_path, int sig)\n{\n    int children_killed = 0;\n\n    FILE *fp = fopen(group_path, \"r\");\n    if (!fp)\n        return children_killed;\n\n    int pid;\n    while (fscanf(fp, \"%d\", &pid) == 1) {\n        INFO(\"  kill -%d %d\", sig, pid);\n        kill(pid, sig);\n        children_killed++;\n    }\n    fclose(fp);\n    return children_killed;\n}\n\nstatic int kill_children(int sig)\n{\n    int children_killed = 0;\n    FOREACH_CONTROLLER {\n        INFO(\"killall -%d from %s\", sig, controller->procfile);\n        children_killed += procfile_killall(controller->procfile, sig);\n    }\n    return children_killed;\n}\n\n#ifdef DEBUG\nstatic void read_proc_cmdline(int pid, char *cmdline)\n{\n    char *cmdline_filename;\n\n    checked_asprintf(&cmdline_filename, \"/proc/%d/cmdline\", pid);\n    FILE *fp = fopen(cmdline_filename, \"r\");\n    if (fp) {\n        size_t len = fread(cmdline, 1, 128, fp);\n        if (len > 0)\n            cmdline[len] = 0;\n        else\n            strcpy(cmdline, \"<NULL>\");\n        fclose(fp);\n    } else {\n        sprintf(cmdline, \"Error reading %s\", cmdline_filename);\n    }\n\n    free(cmdline_filename);\n}\n\nstatic void procfile_dump_children(const char *group_path)\n{\n    INFO(\"---Begin child list for %s\", group_path);\n    FILE *fp = fopen(group_path, \"r\");\n    if (!fp) {\n        INFO(\"Error reading child list!\");\n        return;\n    }\n\n    int pid;\n    while (fscanf(fp, \"%d\", &pid) == 1) {\n        char cmdline[129];\n        read_proc_cmdline(pid, cmdline);\n        INFO(\"  %d: %s\", pid, cmdline);\n    }\n    fclose(fp);\n    INFO(\"---End child list for %s\", group_path);\n}\n\nstatic void dump_all_children_from_cgroups()\n{\n    FOREACH_CONTROLLER {\n        procfile_dump_children(controller->procfile);\n    }\n}\n#endif\n\nstatic void finish_controller_init()\n{\n    FOREACH_CONTROLLER {\n        checked_asprintf(&controller->group_path, \"%s/%s/%s\", CGROUP_MOUNT_PATH, controller->name, cgroup_path);\n        checked_asprintf(&controller->procfile, \"%s/cgroup.procs\", controller->group_path);\n    }\n}\n\nstatic int wait_for_sigchld(pid_t pid_to_match, int timeout_ms)\n{\n    struct pollfd fds[1];\n    fds[0].fd = signal_pipe[0];\n    fds[0].events = POLLIN;\n\n    int end_timeout_us = microsecs() + (1000 * timeout_ms);\n    int next_time_to_wait_ms = timeout_ms;\n    do {\n        INFO(\"poll - %d ms\", next_time_to_wait_ms);\n        if (poll(fds, 1, next_time_to_wait_ms) < 0) {\n            if (errno == EINTR)\n                continue;\n\n            WARN(\"poll\");\n            return -1;\n        }\n\n        if (fds[0].revents) {\n            int signal;\n            ssize_t amt = read(signal_pipe[0], &signal, sizeof(signal));\n            if (amt < 0) {\n                WARN(\"read signal_pipe\");\n                return -1;\n            }\n\n            INFO(\"signal_pipe - SIGNAL %d\", signal);\n            switch (signal) {\n            case SIGCHLD: {\n                int status;\n                pid_t pid = wait(&status);\n                if (pid_to_match == pid) {\n                    INFO(\"cleaned up matching pid %d.\", pid);\n                    return 0;\n                }\n                INFO(\"cleaned up pid %d.\", pid);\n                break;\n            }\n\n            case SIGTERM:\n            case SIGQUIT:\n            case SIGINT:\n                return -1;\n\n            default:\n                WARNX(\"unexpected signal: %d\", signal);\n                return -1;\n            }\n        }\n\n        next_time_to_wait_ms = (end_timeout_us - microsecs()) / 1000;\n    } while (next_time_to_wait_ms > 0);\n\n    INFO(\"timed out waiting for pid %d\", pid_to_match);\n    return -1;\n}\n\nstatic void cleanup_all_children()\n{\n    // In order to cleanup the cgroup, all processes need to exit.\n    // The immediate child of muontrap will have either exited\n    // at this point, so any other processes are orphaned descendents.\n    // I.e., Their parent is now PID 1 and we won't get a SIGCHLD when\n    // they die. We only know who they are since they're in the cgroup.\n\n    // Send every child a SIGKILL\n    int children_left = kill_children(SIGKILL);\n    if (children_left > 0) {\n        INFO(\"Found %d pids and sent them a SIGKILL\", children_left);\n        // poll to see if the cleanup is done every 1 ms\n        int poll_intervals = brutal_kill_wait_ms / 1;\n        do {\n            usleep(1000);\n\n            // Check for children and send SIGKILLs again. This\n            // handles the race where we a new process was spawned\n            // when we iterated through the pids the previous time.\n            children_left = kill_children(SIGKILL);\n            INFO(\"%d pids are still around\", children_left);\n            poll_intervals--;\n        } while (poll_intervals && children_left);\n\n        if (children_left > 0) {\n            WARNX(\"Failed to kill %d pids!\", children_left);\n#ifdef DEBUG\n            dump_all_children_from_cgroups();\n#endif\n        }\n    }\n}\n\nstatic void kill_child_nicely(pid_t child)\n{\n    // Start with SIGTERM\n    int rc = kill(child, SIGTERM);\n    INFO(\"kill -%d %d -> %d (%s)\", SIGTERM, child, rc, rc < 0 ? strerror(errno) : \"success\");\n    if (rc < 0)\n        return;\n\n    // Wait a little for the child to exit\n    if (wait_for_sigchld(child, brutal_kill_wait_ms) < 0) {\n        // Child didn't exit, so SIGKILL it.\n        rc = kill(child, SIGKILL);\n        INFO(\"kill -%d %d -> %d (%s)\", SIGKILL, child, rc, rc < 0 ? strerror(errno) : \"success\");\n        if (rc < 0)\n            return;\n\n        if (wait_for_sigchld(child, brutal_kill_wait_ms) < 0)\n            WARNX(\"SIGKILL didn't work on %d\", child);\n    }\n}\n\nstatic struct controller_info *add_controller(const char *name)\n{\n    // If the controller exists, don't add it twice.\n    for (struct controller_info *c = controllers; c != NULL; c = c->next) {\n        if (strcmp(name, c->name) == 0)\n            return c;\n    }\n\n    struct controller_info *new_controller = malloc(sizeof(struct controller_info));\n    new_controller->name = name;\n    new_controller->group_path = NULL;\n    new_controller->vars = NULL;\n    new_controller->next = controllers;\n    controllers = new_controller;\n\n    return new_controller;\n}\n\nstatic void add_controller_setting(struct controller_info *controller, const char *key, const char *value)\n{\n    struct controller_var *new_var = malloc(sizeof(struct controller_var));\n    new_var->key = key;\n    new_var->value = value;\n    new_var->next = controller->vars;\n    controller->vars = new_var;\n}\n\n#if defined(__linux__)\nstatic int process_stdio(int from_fd)\n{\n    ssize_t written;\n    if (stdio_bytes_avail <= 0)\n        return 0;\n\nretry:\n    written = splice(from_fd, NULL, STDOUT_FILENO, NULL, stdio_bytes_avail, SPLICE_F_MOVE);\n    if (written < 0) {\n        if (errno == EINTR)\n            goto retry;\n\n        WARN(\"failed to splice stdio (%d bytes)\", stdio_bytes_avail);\n        return -1;\n    }\n    stdio_bytes_avail -= written;\n    return 0;\n}\n#else\nstatic int process_stdio(int from_fd)\n{\n    if (stdio_bytes_avail <= 0)\n        return 0;\n\n    size_t max_to_read = stdio_bytes_avail > 4096 ? 4096 : stdio_bytes_avail;\n    char buff[max_to_read];\n    ssize_t got = read(from_fd, buff, max_to_read);\n\n    if (got > 0) {\n        for (ssize_t i = 0; i < got;) {\n            ssize_t written = write(STDOUT_FILENO, &buff[i], got - i);\n\n            if (written <= 0) {\n                if (errno == EINTR)\n                    continue;\n\n                WARN(\"failed to copy stdio\");\n                return -1;\n            }\n            stdio_bytes_avail -= written;\n            i += written;\n        }\n    }\n    return 0;\n}\n#endif\n\nstatic int child_wait_loop(pid_t child_pid, int *still_running)\n{\n    struct pollfd fds[4];\n    fds[0].fd = STDIN_FILENO;\n    fds[0].events = POLLIN | POLLHUP; // POLLERR is implicit\n    fds[1].fd = signal_pipe[0];\n    fds[1].events = POLLIN;\n    fds[2].fd = stdout_pipe[0];\n    fds[2].events = POLLIN;\n    fds[3].fd = stderr_pipe[0];\n    fds[3].events = POLLIN;\n    int poll_num = 2;\n\n    for (;;) {\n        poll_num = 2;\n        // Also poll stdout and optionally stderr when capturing output and accepting stdio data\n        if (capture_stderr_only && stdio_bytes_avail > 0) {\n            // Only polling stderr in stderr-only mode\n            // fds[2] will be stderr_pipe since we're not using stdout_pipe\n            fds[2].fd = stderr_pipe[0];\n            fds[2].events = POLLIN;\n            poll_num++;\n        } else if (capture_output && stdio_bytes_avail > 0) {\n            poll_num++;\n\n            if (capture_stderr)\n                poll_num++;\n        }\n\n        if (poll(fds, poll_num, -1) < 0) {\n            if (errno == EINTR)\n                continue;\n\n            WARN(\"poll\");\n            return EXIT_FAILURE;\n        }\n\n        if (fds[0].revents & POLLHUP) {\n            // Erlang signals that it's done by closing stdin. Exit immediately.\n            INFO(\"stdin closed. Exiting...\");\n            return EXIT_FAILURE;\n        }\n\n        if (fds[0].revents & POLLIN) {\n            uint8_t acknowledgments[32];\n            ssize_t amt = read(STDIN_FILENO, acknowledgments, sizeof(acknowledgments));\n            if (amt >= 0) {\n                // More than one acknowledgment may have come in, so process them all.\n                // NOTE: each ack is 1+its_value\n                int total_acks = amt;\n                for (ssize_t i = 0; i < amt; i++)\n                    total_acks += acknowledgments[i];\n\n                stdio_bytes_avail += total_acks;\n                if (stdio_bytes_avail > stdio_bytes_max) {\n                    WARNX(\"Too many acks %d/%d, got %d\", (int) stdio_bytes_avail, (int) stdio_bytes_max, total_acks);\n                    return EXIT_FAILURE;\n                }\n            } else if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {\n                INFO(\"read STDIN_FILENO error: %s\", strerror(errno));\n                return EXIT_FAILURE;\n            }\n        }\n\n        if (poll_num > 2 && fds[2].revents) {\n            if (process_stdio(fds[2].fd) < 0)\n                return EXIT_FAILURE;\n        }\n\n        if (poll_num > 3 && fds[3].revents) {\n            if (process_stdio(fds[3].fd) < 0)\n                return EXIT_FAILURE;\n        }\n\n        if (fds[1].revents) {\n            int signal;\n            ssize_t amt = read(signal_pipe[0], &signal, sizeof(signal));\n            if (amt < 0) {\n                WARN(\"read signal_pipe\");\n                return EXIT_FAILURE;\n            }\n\n            switch (signal) {\n            case SIGCHLD: {\n                int status;\n                pid_t dying_pid = wait(&status);\n                if (dying_pid == child_pid) {\n                    // Let the caller know that the child isn't running and has been cleaned up\n                    *still_running = 0;\n\n                    int exit_status;\n                    if (WIFSIGNALED(status)) {\n                        // Crash on signal, return the signal in the exit status. See POSIX:\n                        // http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02\n                        exit_status = 128 + WTERMSIG(status);\n                        INFO(\"child terminated via signal %d. our exit status: %d\", status, exit_status);\n                    } else if (WIFEXITED(status)) {\n                        exit_status = WEXITSTATUS(status);\n                        INFO(\"child exited with exit status: %d\", exit_status);\n                    } else {\n                        INFO(\"child terminated with unexpected status: %d\", status);\n                        exit_status = EXIT_FAILURE;\n                    }\n                    return exit_status;\n                } else {\n                    INFO(\"something else caused sigchild: pid=%d, status=%d. our child=%d\", dying_pid, status, child_pid);\n                }\n                break;\n            }\n\n            case SIGTERM:\n            case SIGQUIT:\n            case SIGINT:\n                return EXIT_FAILURE;\n\n            default:\n                WARNX(\"unexpected signal: %d\", signal);\n                return EXIT_FAILURE;\n            }\n        }\n    }\n}\n\nint main(int argc, char *argv[])\n{\n#ifdef DEBUG\n    char filename[64];\n    sprintf(filename, \"muontrap-%d.log\", getpid());\n    debug_fp = fopen(filename, \"w\");\n    if (!debug_fp)\n        debug_fp = stderr;\n#endif\n    INFO(\"muontrap argc=%d\", argc);\n    if (argc == 1) {\n        usage();\n        exit(EXIT_FAILURE);\n    }\n\n    int opt;\n    char *argv0 = NULL;\n    struct controller_info *current_controller = NULL;\n    while ((opt = getopt_long(argc, argv, \"a:c:g:hk:s:0:\", long_options, NULL)) != -1) {\n        switch (opt) {\n        case 'a': // --gid\n        {\n            char *endptr;\n            run_as_gid = strtoul(optarg, &endptr, 0);\n            if (*endptr != '\\0') {\n                struct group *group = getgrnam(optarg);\n                if (!group)\n                    FATALX(\"Unknown group '%s'\", optarg);\n                run_as_gid = group->gr_gid;\n            }\n            if (run_as_gid == 0)\n                FATALX(\"Setting the group to root or gid 0 is not allowed\");\n            break;\n        }\n\n        case 'c':\n            current_controller = add_controller(optarg);\n            break;\n\n        case 'g':\n            if (cgroup_path)\n                FATALX(\"Only one cgroup group_path supported.\");\n            cgroup_path = optarg;\n            break;\n\n        case 'h':\n            usage();\n            exit(EXIT_SUCCESS);\n\n        case 'k': // --delay-to-sigkill\n            brutal_kill_wait_ms = strtoul(optarg, NULL, 0);\n            break;\n\n        case 'l': // --stdio-window\n            stdio_bytes_max = strtol(optarg, NULL, 0);\n            if (stdio_bytes_max < 16)\n                stdio_bytes_max = 16;\n\n            stdio_bytes_avail = stdio_bytes_max;\n            break;\n\n        case 'o': // --capture-output\n            capture_output = 1;\n            break;\n\n        case 'e': // --capture-stderr\n            capture_stderr = 1;\n            break;\n\n        case 'r': // --capture-stderr-only\n            capture_stderr_only = 1;\n            break;\n\n        case 's':\n        {\n            if (!current_controller)\n                FATALX(\"Specify a cgroup controller (-c) before setting a variable\");\n\n            char *equalsign = strchr(optarg, '=');\n            if (!equalsign)\n                FATALX(\"No '=' found when setting a variable: '%s'\", optarg);\n\n            // NULL terminate the key. We can do this since we're already modifying\n            // the arguments by using getopt.\n            *equalsign = '\\0';\n            add_controller_setting(current_controller, optarg, equalsign + 1);\n            break;\n        }\n\n        case 'u': // --uid\n        {\n            char *endptr;\n            run_as_uid = strtoul(optarg, &endptr, 0);\n            if (*endptr != '\\0') {\n                struct passwd *passwd = getpwnam(optarg);\n                if (!passwd)\n                    FATALX(\"Unknown user '%s'\", optarg);\n                run_as_uid = passwd->pw_uid;\n            }\n            if (run_as_uid == 0)\n                FATALX(\"Setting the user to root or uid 0 is not allowed\");\n            break;\n        }\n\n        case '0': // --argv0\n            argv0 = optarg;\n            break;\n\n        default:\n            usage();\n            exit(EXIT_FAILURE);\n        }\n    }\n\n    if (argc == optind)\n        FATALX(\"Specify a program to run\");\n\n    if (cgroup_path == NULL && controllers)\n        FATALX(\"Specify a cgroup group_path (-g)\");\n\n    if (cgroup_path && !controllers)\n        FATALX(\"Specify a cgroup controller (-c) if you specify a group_path\");\n\n    finish_controller_init();\n\n    // Finished processing commandline. Initialize and run child.\n\n    if (pipe(signal_pipe) < 0)\n        FATAL(\"pipe\");\n    if (fcntl(signal_pipe[0], F_SETFD, FD_CLOEXEC) < 0 ||\n        fcntl(signal_pipe[1], F_SETFD, FD_CLOEXEC) < 0)\n        WARN(\"fcntl(FD_CLOEXEC)\");\n\n    if (capture_stderr_only) {\n        // Only capturing stderr, create stderr pipe\n        if (pipe(stderr_pipe) < 0)\n            FATAL(\"pipe\");\n        if (fcntl(stderr_pipe[0], F_SETFD, FD_CLOEXEC) < 0 ||\n            fcntl(stderr_pipe[1], F_SETFD, FD_CLOEXEC) < 0)\n            WARN(\"fcntl(FD_CLOEXEC)\");\n    } else if (capture_output) {\n        if (pipe(stdout_pipe) < 0)\n            FATAL(\"pipe\");\n        if (fcntl(stdout_pipe[0], F_SETFD, FD_CLOEXEC) < 0 ||\n            fcntl(stdout_pipe[1], F_SETFD, FD_CLOEXEC) < 0)\n            WARN(\"fcntl(FD_CLOEXEC)\");\n\n        if (capture_stderr) {\n            if (pipe(stderr_pipe) < 0)\n                FATAL(\"pipe\");\n            if (fcntl(stderr_pipe[0], F_SETFD, FD_CLOEXEC) < 0 ||\n                fcntl(stderr_pipe[1], F_SETFD, FD_CLOEXEC) < 0)\n                WARN(\"fcntl(FD_CLOEXEC)\");\n        }\n    }\n\n    enable_signal_handlers();\n\n    create_cgroups();\n\n    update_cgroup_settings();\n\n    const char *program_name = argv[optind];\n    if (argv0)\n        argv[optind] = argv0;\n    pid_t pid = fork_exec(program_name, &argv[optind]);\n\n    int still_running = 1;\n    int exit_status = child_wait_loop(pid, &still_running);\n\n    if (still_running) {\n        // Kill our immediate child if it's still running\n        kill_child_nicely(pid);\n    }\n\n    // Cleanup all descendents if using cgroups\n    cleanup_all_children();\n\n    destroy_cgroups();\n    disable_signal_handlers();\n\n    exit(exit_status);\n}\n"
  },
  {
    "path": "lib/muontrap/cgroups.ex",
    "content": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n#\n# SPDX-License-Identifier: Apache-2.0\n\ndefmodule MuonTrap.Cgroups do\n  @moduledoc false\n\n  @cgroup_fs \"/sys/fs/cgroup\"\n\n  @doc \"\"\"\n  Return true if it looks like the system has cgroups support enabled\n  \"\"\"\n  @spec cgroups_enabled?() :: boolean()\n  def cgroups_enabled?() do\n    case get_controllers() do\n      {:ok, []} -> false\n      {:ok, _list} -> true\n      {:error, _anything} -> false\n    end\n  end\n\n  @doc \"\"\"\n  Return a list available cgroup controllers\n  \"\"\"\n  @spec get_controllers() :: {:ok, [String.t()]} | {:error, :enoent}\n  def get_controllers() do\n    File.ls(\"/sys/fs/cgroup\")\n  end\n\n  @doc \"\"\"\n  Get a cgroup variable (like cgget)\n  \"\"\"\n  @spec cgget(String.t(), String.t(), String.t()) :: {:ok, String.t()} | {:error, File.posix()}\n  def cgget(controller, cgroup_path, variable_name) do\n    path = Path.join([@cgroup_fs, controller, cgroup_path, variable_name])\n    File.read(path)\n  end\n\n  @doc \"\"\"\n  Set a cgroup variable (like cgset)\n  \"\"\"\n  @spec cgset(String.t(), String.t(), String.t(), String.t()) :: :ok | {:error, File.posix()}\n  def cgset(controller, cgroup_path, variable_name, value) do\n    path = Path.join([@cgroup_fs, controller, cgroup_path, variable_name])\n    File.write(path, value)\n  end\nend\n"
  },
  {
    "path": "lib/muontrap/daemon.ex",
    "content": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2018 Matt Ludwigs\n# SPDX-FileCopyrightText: 2021 Aldebaran Alonso\n# SPDX-FileCopyrightText: 2023 Eric Rauer\n# SPDX-FileCopyrightText: 2023 Jon Carstens\n# SPDX-FileCopyrightText: 2024 Ben Youngblood\n# SPDX-FileCopyrightText: 2024 Milan Vit\n# SPDX-FileCopyrightText: 2025 Fernando Mumbach\n#\n# SPDX-License-Identifier: Apache-2.0\n\ndefmodule MuonTrap.Daemon do\n  @moduledoc \"\"\"\n  Wrap an OS process in a GenServer so that it can be supervised.\n\n  For example, in your children list add MuonTrap.Daemon like this:\n\n  ```elixir\n  children = [\n    {MuonTrap.Daemon, [\"my_server\", [\"--options\", \"foo\"], [cd: \"/some_directory\"]]}\n  ]\n\n  opts = [strategy: :one_for_one, name: MyApplication.Supervisor]\n  Supervisor.start_link(children, opts)\n  ```\n\n  In the `child_spec` tuple, the second element is a list that corresponds to\n  the `MuonTrap.cmd/3` parameters. I.e., The first item in the list is the\n  program to run, the second is a list of commandline arguments, and the third\n  is a list of options. The same options as `MuonTrap.cmd/3` are available with\n  the following additions:\n\n  * `:name` - Name the Daemon GenServer\n  * `:logger_fun` - Pass a 1-arity function or `t:mfargs/0` tuple to replace\n    the default logging behavior. When set, `:log_output`, `:log_prefix`,\n    `:log_transform`,\n    and `:logger_metadata` will be ignored.\n  * `:log_output` - When set, send output from the command to the Logger.\n    Specify the log level (e.g., `:debug`)\n  * `:log_prefix` - Prefix each log message with this string (defaults to the\n    program's path)\n  * `:log_transform` - Pass a function that takes a string and returns a string\n    to format output from the command. Defaults to `String.replace_invalid/1`\n    on Elixir 1.16+ to avoid crashing the logger on non-UTF8 output.\n  * `:logger_metadata` - A keyword list to merge into the process's logger metadata.\n    The `:muontrap_cmd` and `:muontrap_args` keys are automatically added and\n    cannot be overridden.\n  * `:stderr_to_stdout` - When set to `true`, redirect stderr to stdout.\n    Defaults to `false`.\n  * `:capture_stderr_only` - When set to `true`, capture only stderr and ignore stdout.\n    This is useful when you want to capture error messages but not regular output.\n    Defaults to `false`.\n  * `:exit_status_to_reason` - Optional function to convert the exit status (a\n    number) to stop reason for the Daemon GenServer. Use if error exit codes\n    carry information or aren't errors.\n\n  If you want to run multiple `MuonTrap.Daemon`s under one supervisor, they'll\n  all need unique IDs. Use `Supervisor.child_spec/2` like this:\n\n  ```elixir\n  Supervisor.child_spec({MuonTrap.Daemon, [\"my_server\", []]}, id: :server1)\n  ```\n  \"\"\"\n  use GenServer\n\n  alias MuonTrap.Cgroups\n\n  require Logger\n\n  defstruct [\n    :buffer,\n    :command,\n    :port,\n    :cgroup_path,\n    :logger_fun,\n    :exit_status_to_reason,\n    :output_byte_count\n  ]\n\n  @max_data_to_buffer 256\n\n  @spec child_spec(keyword()) :: Supervisor.child_spec()\n  def child_spec([command, args]) do\n    child_spec([command, args, []])\n  end\n\n  def child_spec([command, args, opts]) do\n    %{\n      id: __MODULE__,\n      start: {__MODULE__, :start_link, [command, args, opts]},\n      type: :worker,\n      restart: :permanent,\n      shutdown: 500\n    }\n  end\n\n  @doc \"\"\"\n  Start/link a deamon GenServer for the specified command.\n  \"\"\"\n  @spec start_link(binary(), [binary()], keyword()) :: GenServer.on_start()\n  def start_link(command, args, opts \\\\ []) do\n    {genserver_opts, opts} =\n      case Keyword.pop(opts, :name) do\n        {nil, _opts} -> {[], opts}\n        {name, new_opts} -> {[name: name], new_opts}\n      end\n\n    GenServer.start_link(__MODULE__, [command, args, opts], genserver_opts)\n  end\n\n  @doc \"\"\"\n  Get the value of the specified cgroup variable.\n  \"\"\"\n  @spec cgget(GenServer.server(), binary(), binary()) ::\n          {:ok, String.t()} | {:error, File.posix()}\n  def cgget(server, controller, variable_name) do\n    GenServer.call(server, {:cgget, controller, variable_name})\n  end\n\n  @doc \"\"\"\n  Modify a cgroup variable.\n  \"\"\"\n  @spec cgset(GenServer.server(), binary(), binary(), binary()) :: :ok | {:error, File.posix()}\n  def cgset(server, controller, variable_name, value) do\n    GenServer.call(server, {:cgset, controller, variable_name, value})\n  end\n\n  @doc \"\"\"\n  Return the OS pid to the muontrap executable.\n  \"\"\"\n  @spec os_pid(GenServer.server()) :: non_neg_integer() | :error\n  def os_pid(server) do\n    GenServer.call(server, :os_pid)\n  end\n\n  @doc \"\"\"\n  Return statistics about the daemon\n\n  Statistics:\n\n  * `:output_byte_count` - bytes output by the process being run\n  \"\"\"\n  @spec statistics(GenServer.server()) :: %{output_byte_count: non_neg_integer()}\n  def statistics(server) do\n    GenServer.call(server, :statistics)\n  end\n\n  @impl GenServer\n  def init([command, args, opts]) do\n    options = MuonTrap.Options.validate(:daemon, command, args, opts)\n    port_options = MuonTrap.Port.port_options(options) ++ [:stream]\n\n    port = Port.open({:spawn_executable, to_charlist(MuonTrap.muontrap_path())}, port_options)\n\n    # Logger.metadata/0 has a side effect to set the metadata for the current process\n    options\n    |> Map.get(:logger_metadata, [])\n    |> Keyword.merge(muontrap_cmd: command, muontrap_args: Enum.join(args, \" \"))\n    |> Logger.metadata()\n\n    {:ok,\n     %__MODULE__{\n       buffer: \"\",\n       command: command,\n       port: port,\n       cgroup_path: Map.get(options, :cgroup_path),\n       logger_fun: logger_fun(options, command),\n       exit_status_to_reason:\n         Map.get(options, :exit_status_to_reason, fn _ -> :error_exit_status end),\n       output_byte_count: 0\n     }}\n  end\n\n  defp logger_fun(%{logger_fun: fun}, _command) when is_function(fun, 1), do: fun\n  defp logger_fun(%{logger_fun: {m, f, a}}, _command), do: &apply(m, f, [&1 | a])\n\n  defp logger_fun(options, command) do\n    log_output = Map.get(options, :log_output)\n\n    if log_output == nil do\n      fn _line -> :ok end\n    else\n      log_prefix = Map.get(options, :log_prefix, command <> \": \")\n      log_transform = Map.get(options, :log_transform, &default_transform/1)\n\n      fn line ->\n        Logger.log(log_output, [log_prefix, log_transform.(line)])\n      end\n    end\n  end\n\n  if Version.match?(System.version(), \">= 1.16.0\") do\n    defp default_transform(line) do\n      String.replace_invalid(line)\n    end\n  else\n    defp default_transform(line) do\n      if String.valid?(line) do\n        line\n      else\n        \"** MuonTrap filtered #{byte_size(line)} non-UTF8 bytes **\"\n      end\n    end\n  end\n\n  @impl GenServer\n  def handle_call({:cgget, controller, variable_name}, _from, %{cgroup_path: cgroup_path} = state) do\n    result = Cgroups.cgget(controller, cgroup_path, variable_name)\n\n    {:reply, result, state}\n  end\n\n  def handle_call(\n        {:cgset, controller, variable_name, value},\n        _from,\n        %{cgroup_path: cgroup_path} = state\n      ) do\n    result = Cgroups.cgset(controller, cgroup_path, variable_name, value)\n\n    {:reply, result, state}\n  end\n\n  def handle_call(:os_pid, _from, state) do\n    os_pid =\n      case Port.info(state.port, :os_pid) do\n        {:os_pid, p} -> p\n        nil -> :error\n      end\n\n    {:reply, os_pid, state}\n  end\n\n  def handle_call(:statistics, _from, state) do\n    statistics = %{output_byte_count: state.output_byte_count}\n    {:reply, statistics, state}\n  end\n\n  @impl GenServer\n  def handle_info({port, {:data, message}}, %__MODULE__{port: port} = state) do\n    bytes_received = byte_size(message)\n    state = split_and_log(message, state)\n\n    MuonTrap.Port.report_bytes_handled(state.port, bytes_received)\n\n    {:noreply, %{state | output_byte_count: state.output_byte_count + bytes_received}}\n  end\n\n  def handle_info({port, {:exit_status, status}}, %__MODULE__{port: port} = state) do\n    reason =\n      case status do\n        0 ->\n          Logger.info(\"#{state.command}: Process exited successfully\")\n          :normal\n\n        _failure ->\n          Logger.error(\"#{state.command}: Process exited with status #{status}\")\n          state.exit_status_to_reason.(status)\n      end\n\n    {:stop, reason, state}\n  end\n\n  def handle_info(_message, state) do\n    {:noreply, state}\n  end\n\n  defp split_and_log(data, state) do\n    {lines, remainder} = process_data(state.buffer <> data)\n\n    Enum.each(lines, &state.logger_fun.(&1))\n\n    %{state | buffer: remainder}\n  end\n\n  @doc false\n  @spec process_data(binary()) :: {[String.t()], binary()}\n  def process_data(data) do\n    data |> String.split(\"\\n\") |> process_lines([])\n  end\n\n  defp process_lines([leftovers], acc) do\n    {Enum.reverse(acc), trim_buffer(leftovers)}\n  end\n\n  defp process_lines([line | rest], acc) do\n    process_lines(rest, [line | acc])\n  end\n\n  defp trim_buffer(data) when byte_size(data) > @max_data_to_buffer,\n    do: :binary.part(data, 0, @max_data_to_buffer)\n\n  defp trim_buffer(data), do: data\nend\n"
  },
  {
    "path": "lib/muontrap/options.ex",
    "content": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2023 Ben Youngblood\n# SPDX-FileCopyrightText: 2023 Eric Rauer\n# SPDX-FileCopyrightText: 2023 Jon Carstens\n#\n# SPDX-License-Identifier: Apache-2.0\n\ndefmodule MuonTrap.Options do\n  @moduledoc \"\"\"\n  Validate and normalize the options passed to MuonTrap.cmd/3 and MuonTrap.Daemon.start_link/3\n\n  This module is generally not called directly, but it's likely\n  the source of exceptions if any options aren't quite right. Call `validate/4` directly to\n  debug or check options without invoking a command.\n  \"\"\"\n\n  @typedoc \"\"\"\n  The following fields are always present:\n\n  * `:cmd` - the command to run\n  * `:args` - a list of arguments to the command\n\n  The next fields are optional:\n\n  * `:into` - `MuonTrap.cmd/3` only\n  * `:cd`\n  * `:arg0`\n  * `:stderr_to_stdout`\n  * `:capture_stderr_only`\n  * `:parallelism`\n  * `:env`\n  * `:name` - `MuonTrap.Daemon`-only\n  * `:logger_fun` - `MuonTrap.Daemon`-only\n  * `:log_output` - `MuonTrap.Daemon`-only, ignored if logger_fun is set\n  * `:log_prefix` - `MuonTrap.Daemon`-only, ignored if logger_fun is set\n  * `:log_transform` - `MuonTrap.Daemon`-only, ignored if logger_fun is set\n  * `:logger_metadata` - `MuonTrap.Daemon`-only, ignored if logger_fun is set and doesn't call the Elixir Logger\n  * `:stdio_window`\n  * `:exit_status_to_reason` - `MuonTrap.Daemon`-only\n  * `:cgroup_controllers`\n  * `:cgroup_path`\n  * `:cgroup_base`\n  * `:delay_to_sigkill`\n  * `:cgroup_sets`\n  * `:uid`\n  * `:gid`\n  * `:timeout` - `MuonTrap.cmd/3` only\n\n  \"\"\"\n  @type t() :: map()\n\n  # See https://hexdocs.pm/logger/Logger.html#module-levels\n  # Include `:warn` for older Elixir versions\n  @log_levels [:emergency, :alert, :critical, :error, :warning, :warn, :notice, :info, :debug]\n\n  @doc \"\"\"\n  Validate options and normalize them for invoking commands\n\n  Pass in `:cmd` or `:daemon` for the first parameter to allow function-specific\n  options.\n  \"\"\"\n  @spec validate(:cmd | :daemon, binary(), [binary()], keyword()) :: t()\n  def validate(context, cmd, args, opts) when context in [:cmd, :daemon] do\n    assert_no_null_byte!(cmd, context)\n\n    if !Enum.all?(args, &is_binary/1) do\n      raise ArgumentError, \"all arguments for #{operation(context)} must be binaries\"\n    end\n\n    abs_command = System.find_executable(cmd) || :erlang.error(:enoent, [cmd, args, opts])\n\n    validate_options(context, abs_command, args, opts)\n    |> resolve_cgroup_path()\n  end\n\n  defp resolve_cgroup_path(%{cgroup_path: _path, cgroup_base: _base}) do\n    raise ArgumentError, \"cannot specify both a cgroup_path and a cgroup_base\"\n  end\n\n  defp resolve_cgroup_path(%{cgroup_base: base} = options) do\n    # Create a random subfolder for this invocation\n    Map.put(options, :cgroup_path, Path.join(base, random_string()))\n  end\n\n  defp resolve_cgroup_path(other), do: other\n\n  # Thanks https://github.com/danhper/elixir-temp/blob/master/lib/temp.ex\n  defp random_string() do\n    Integer.to_string(:rand.uniform(0x100000000), 36) |> String.downcase()\n  end\n\n  defp validate_options(context, cmd, args, opts) do\n    Enum.reduce(\n      opts,\n      %{cmd: cmd, args: args, into: \"\"},\n      &validate_option(context, &1, &2)\n    )\n  end\n\n  # System.cmd/3 options\n  defp validate_option(:cmd, {:into, what}, opts), do: Map.put(opts, :into, what)\n  defp validate_option(_any, {:cd, bin}, opts) when is_binary(bin), do: Map.put(opts, :cd, bin)\n\n  defp validate_option(_any, {:arg0, bin}, opts) when is_binary(bin),\n    do: Map.put(opts, :arg0, bin)\n\n  defp validate_option(_any, {:stderr_to_stdout, bool}, opts) when is_boolean(bool),\n    do: Map.put(opts, :stderr_to_stdout, bool)\n\n  defp validate_option(_any, {:capture_stderr_only, bool}, opts) when is_boolean(bool),\n    do: Map.put(opts, :capture_stderr_only, bool)\n\n  defp validate_option(_any, {:parallelism, bool}, opts) when is_boolean(bool),\n    do: Map.put(opts, :parallelism, bool)\n\n  defp validate_option(_any, {:env, enum}, opts),\n    do: Map.put(opts, :env, validate_env(enum))\n\n  # MuonTrap.Daemon options\n  defp validate_option(:daemon, {:name, name}, opts),\n    do: Map.put(opts, :name, name)\n\n  defp validate_option(:daemon, {:log_output, level}, opts) when level in @log_levels,\n    do: Map.put(opts, :log_output, level)\n\n  defp validate_option(:daemon, {:log_prefix, prefix}, opts) when is_binary(prefix),\n    do: Map.put(opts, :log_prefix, prefix)\n\n  defp validate_option(:daemon, {:log_transform, log_transform}, opts)\n       when is_function(log_transform),\n       do: Map.put(opts, :log_transform, log_transform)\n\n  defp validate_option(:daemon, {:logger_metadata, metadata}, opts) when is_list(metadata),\n    do: Map.put(opts, :logger_metadata, metadata)\n\n  defp validate_option(:daemon, {:logger_fun, logger}, opts) when is_function(logger, 1),\n    do: Map.put(opts, :logger_fun, logger)\n\n  defp validate_option(:daemon, {:logger_fun, {m, f, a}}, opts)\n       when is_atom(m) and is_atom(f) and is_list(a),\n       do: Map.put(opts, :logger_fun, {m, f, a})\n\n  defp validate_option(:daemon, {:logger_fun, {m, f}}, opts)\n       when is_atom(m) and is_atom(f),\n       do: Map.put(opts, :logger_fun, {m, f, []})\n\n  defp validate_option(:daemon, {:logger_fun, v}, _opts),\n    do:\n      raise(\n        ArgumentError,\n        \"invalid option :logger_fun with value #{inspect(v)}, expected a 1-arity function or an mfa tuple\"\n      )\n\n  defp validate_option(_any, {:stdio_window, count}, opts) when is_integer(count),\n    do: Map.put(opts, :stdio_window, count)\n\n  defp validate_option(:daemon, {:exit_status_to_reason, exit_status_to_reason}, opts)\n       when is_function(exit_status_to_reason),\n       do: Map.put(opts, :exit_status_to_reason, exit_status_to_reason)\n\n  # MuonTrap common options\n  defp validate_option(_any, {:cgroup_controllers, controllers}, opts) when is_list(controllers),\n    do: Map.put(opts, :cgroup_controllers, controllers)\n\n  defp validate_option(_any, {:cgroup_path, path}, opts) when is_binary(path) do\n    Map.put(opts, :cgroup_path, path)\n  end\n\n  defp validate_option(_any, {:cgroup_base, path}, opts) when is_binary(path) do\n    Map.put(opts, :cgroup_base, path)\n  end\n\n  defp validate_option(_any, {:delay_to_sigkill, delay}, opts) when is_integer(delay),\n    do: Map.put(opts, :delay_to_sigkill, delay)\n\n  defp validate_option(_any, {:cgroup_sets, sets}, opts) when is_list(sets),\n    do: Map.put(opts, :cgroup_sets, sets)\n\n  defp validate_option(_any, {:uid, id}, opts) when is_integer(id) or is_binary(id),\n    do: Map.put(opts, :uid, id)\n\n  defp validate_option(_any, {:gid, id}, opts) when is_integer(id) or is_binary(id),\n    do: Map.put(opts, :gid, id)\n\n  defp validate_option(:cmd, {:timeout, timeout}, opts) when is_integer(timeout) and timeout > 0,\n    do: Map.put(opts, :timeout, timeout)\n\n  defp validate_option(_any, {key, val}, _opts),\n    do: raise(ArgumentError, \"invalid option #{inspect(key)} with value #{inspect(val)}\")\n\n  defp validate_env(enum) do\n    Enum.map(enum, fn\n      {k, nil} ->\n        {String.to_charlist(k), false}\n\n      {k, v} ->\n        {String.to_charlist(k), String.to_charlist(v)}\n\n      other ->\n        raise ArgumentError, \"invalid environment key-value #{inspect(other)}\"\n    end)\n  end\n\n  # Copied from Elixir's system.ex to make MuonTrap.cmd pass System.cmd's tests\n  defp assert_no_null_byte!(binary, context) do\n    case :binary.match(binary, \"\\0\") do\n      {_, _} ->\n        raise ArgumentError,\n              \"cannot execute #{operation(context)} for program with null byte, got: #{inspect(binary)}\"\n\n      :nomatch ->\n        :ok\n    end\n  end\n\n  defp operation(:cmd), do: \"MuonTrap.cmd/3\"\n  defp operation(:daemon), do: \"MuonTrap.Daemon.start_link/3\"\nend\n"
  },
  {
    "path": "lib/muontrap/port.ex",
    "content": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2023 Ben Youngblood\n# SPDX-FileCopyrightText: 2023 Jon Carstens\n#\n# SPDX-License-Identifier: Apache-2.0\n\ndefmodule MuonTrap.Port do\n  @moduledoc false\n\n  @spec muontrap_path() :: String.t()\n  def muontrap_path() do\n    Application.app_dir(:muontrap, [\"priv\", \"muontrap\"])\n  end\n\n  @doc \"\"\"\n  Run a command in a similar way to System.cmd/3, but taking MuonTrap options\n\n  This code is mostly copy/pasted from System.cmd/3's implementation so that\n  it works similarly.\n  \"\"\"\n  @spec cmd(MuonTrap.Options.t()) ::\n          {Collectable.t(), exit_status :: non_neg_integer() | :timeout}\n  def cmd(options) do\n    opts = port_options(options, [\"--capture-output\"])\n    {initial, fun} = Collectable.into(options.into)\n    {maybe_timer, timeout_message} = maybe_start_timer(options[:timeout])\n\n    try do\n      port = Port.open({:spawn_executable, to_charlist(muontrap_path())}, opts)\n      do_cmd(port, initial, fun, timeout_message)\n    catch\n      kind, reason ->\n        fun.(initial, :halt)\n        :erlang.raise(kind, reason, __STACKTRACE__)\n    else\n      {acc, status} -> {fun.(acc, :done), status}\n    after\n      maybe_stop_timer(maybe_timer, timeout_message)\n    end\n  end\n\n  defp do_cmd(port, acc, fun, timeout_message) do\n    receive do\n      {^port, {:data, data}} ->\n        report_bytes_handled(port, byte_size(data))\n        do_cmd(port, fun.(acc, {:cont, data}), fun, timeout_message)\n\n      {^port, {:exit_status, status}} ->\n        {acc, status}\n\n      ^timeout_message ->\n        Port.close(port)\n        {acc, :timeout}\n    end\n  end\n\n  @spec port_options(MuonTrap.Options.t(), [String.t()]) :: list()\n  def port_options(options, args \\\\ []) do\n    [\n      :use_stdio,\n      :exit_status,\n      :binary,\n      :hide,\n      {:args, args ++ muontrap_args(options)} | Enum.flat_map(options, &port_option/1)\n    ]\n  end\n\n  defp muontrap_args(options) do\n    Enum.flat_map(options, &muontrap_arg/1) ++ [\"--\", options.cmd] ++ options.args\n  end\n\n  defp muontrap_arg({:cgroup_path, path}), do: [\"--group\", path]\n  defp muontrap_arg({:delay_to_sigkill, delay}), do: [\"--delay-to-sigkill\", to_string(delay)]\n  defp muontrap_arg({:uid, id}), do: [\"--uid\", to_string(id)]\n  defp muontrap_arg({:gid, id}), do: [\"--gid\", to_string(id)]\n  defp muontrap_arg({:arg0, arg0}), do: [\"--arg0\", arg0]\n  defp muontrap_arg({:stdio_window, count}), do: [\"--stdio-window\", to_string(count)]\n  defp muontrap_arg({:stderr_to_stdout, true}), do: [\"--capture-stderr\"]\n  defp muontrap_arg({:capture_stderr_only, true}), do: [\"--capture-stderr-only\"]\n\n  defp muontrap_arg({log_opt, _}) when log_opt in [:log_output, :logger_fun],\n    do: [\"--capture-output\"]\n\n  defp muontrap_arg({:cgroup_controllers, controllers}) do\n    Enum.flat_map(controllers, fn controller -> [\"--controller\", controller] end)\n  end\n\n  defp muontrap_arg({:cgroup_sets, sets}) do\n    Enum.flat_map(sets, fn {controller, variable, value} ->\n      [\"--controller\", controller, \"--set\", \"#{variable}=#{value}\"]\n    end)\n  end\n\n  defp muontrap_arg(_other), do: []\n\n  defp port_option({:env, env}), do: [{:env, env}]\n  defp port_option({:cd, bin}), do: [{:cd, bin}]\n  defp port_option({:arg0, bin}), do: [{:arg0, bin}]\n  defp port_option({:parallelism, bool}), do: [{:parallelism, bool}]\n  defp port_option(_other), do: []\n\n  @spec report_bytes_handled(port(), pos_integer()) :: :ok\n  def report_bytes_handled(port, count) when is_port(port) and is_integer(count) do\n    cmd = encode_acks(count)\n    _ = Port.command(port, cmd)\n    :ok\n  rescue\n    # A process may attempt to mark the bytes processed after the port has\n    # closed but before it received an :exit_status message. In those cases\n    # the command will fail with ArgumentError, but should be safe to\n    # ignore since we don't need to report anymore\n    ArgumentError -> :ok\n  end\n\n  # Each acknowledgment is one unsigned byte that's the number of bytes to acknowledge\n  # plus 1. E.g., 0 means to acknowledge 1 byte. 255 means to acknowledge 256 bytes.\n  @spec encode_acks(pos_integer()) :: iodata()\n  def encode_acks(count) when count > 0 do\n    full_acks = div(count, 256)\n    partial_acks = rem(count, 256)\n    encode_acks_helper(full_acks, partial_acks)\n  end\n\n  defp encode_acks_helper(0, partial_acks), do: <<partial_acks - 1>>\n  defp encode_acks_helper(full_acks, 0), do: :binary.copy(<<255>>, full_acks)\n\n  defp encode_acks_helper(full_acks, partial_acks),\n    do: [:binary.copy(<<255>>, full_acks), partial_acks - 1]\n\n  @spec maybe_start_timer(non_neg_integer() | nil) :: {reference() | nil, {:timeout, reference()}}\n  defp maybe_start_timer(timeout) when is_integer(timeout) do\n    timeout_message = {:timeout, make_ref()}\n    timer_ref = Process.send_after(self(), timeout_message, timeout)\n    {timer_ref, timeout_message}\n  end\n\n  # When not setting a timer, return a fake message. This simplifies pattern\n  # matching in cmd/1 and do_cmd/4.\n  defp maybe_start_timer(_), do: {nil, {:timeout, make_ref()}}\n\n  @spec maybe_stop_timer(reference() | nil, {:timeout, reference()}) :: :ok\n  defp maybe_stop_timer(nil, _), do: :ok\n\n  defp maybe_stop_timer(timer_ref, timeout_message) do\n    # Ensure we capture the timeout message in case it arrives around the same\n    # time the command completes.\n    if Process.cancel_timer(timer_ref) == false do\n      receive do\n        ^timeout_message -> :ok\n      after\n        0 -> :ok\n      end\n    end\n\n    :ok\n  end\nend\n"
  },
  {
    "path": "lib/muontrap.ex",
    "content": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2023 Ben Youngblood\n#\n# SPDX-License-Identifier: Apache-2.0\n\ndefmodule MuonTrap do\n  @moduledoc \"\"\"\n  MuonTrap protects you from lost and out of control OS processes.\n\n  You can use it as a `System.cmd/3` replacement or to pull OS processes into\n  an Erlang supervision tree via `MuonTrap.Daemon`. Either way, if the Erlang\n  process that runs the command dies, then the OS processes will die as well.\n\n  MuonTrap tries very hard to kill OS processes so that remnants don't hang\n  around the system when your Erlang code thinks they should be gone. MuonTrap\n  can use the Linux kernel's `cgroup` feature to contain the child process and\n  all of its children. From there, you can limit CPU and memory and other\n  resources to the process group.\n\n  MuonTrap does not require `cgroups` but keep in mind that OS processes can\n  escape. It is, however, still an improvement over `System.cmd/3` which does\n  not have a mechanism for dealing it OS processes that do not monitor their\n  stdin for when to close.\n\n  For more information, see the documentation for `MuonTrap.cmd/3` and\n  `MuonTrap.Daemon`\n\n  ## Configuring cgroups\n\n  On most Linux distributions, use `cgcreate` to create a new cgroup.  You can\n  name them almost anything. The command below creates one named `muontrap` for\n  the current user. It supports memory and CPU controls.\n\n  ```sh\n  sudo cgcreate -a $(whoami) -g memory,cpu:muontrap\n  ```\n\n  Nerves systems do not contain `cgcreate` by default. Due to the simpler Linux\n  setup, it may be sufficient to run `File.mkdir_p(cgroup_path)` to create a\n  cgroup. For example:\n\n  ```elixir\n  File.mkdir_p(\"/sys/fs/cgroup/memory/muontrap\")\n  ```\n\n  This creates the cgroup path, `muontrap` under the `memory` controller.  If\n  you do not have the `\"/sys/fs/cgroup\"` directory, you will need to mount it\n  or update your `erlinit.config` to mount it for you. See a newer official\n  system for an example.\n  \"\"\"\n\n  @doc ~S\"\"\"\n  Executes a command like `System.cmd/3` via the `muontrap` wrapper.\n\n  ## Options\n\n    * `:cgroup_controllers` - run the command under the specified cgroup controllers. Defaults to `[]`.\n    * `:cgroup_base` - create a temporary path under the specified cgroup path\n    * `:cgroup_path` - explicitly specify a path to use. Use `:cgroup_base`, unless you must control the path.\n    * `:cgroup_sets` - set a cgroup controller parameter before running the command\n    * `:delay_to_sigkill` - milliseconds before sending a SIGKILL to a child process if it doesn't exit with a SIGTERM (default 500 ms)\n    * `:uid` - run the command using the specified uid or username\n    * `:gid` - run the command using the specified gid or group\n    * `:timeout` - milliseconds to wait for the command to complete. If the\n      command does not exit before the timeout, the return value will contain\n      the output up to that point and `:timeout` as the exit status. The child\n      process will be sent SIGTERM\n\n  The following `System.cmd/3` options are also available:\n\n    * `:into` - injects the result into the given collectable, defaults to `\"\"`\n    * `:cd` - the directory to run the command in\n    * `:env` - an enumerable of tuples containing environment key-value as binary\n    * `:arg0` - sets the command arg0\n    * `:stderr_to_stdout` - redirects stderr to stdout when `true`\n    * `:capture_stderr_only` - when `true`, captures only stderr and ignores stdout (useful for capturing errors while ignoring normal output)\n    * `:parallelism` - when `true`, the VM will schedule port tasks to improve\n      parallelism in the system. If set to `false`, the VM will try to perform\n      commands immediately, improving latency at the expense of parallelism.\n      The default can be set on system startup by passing the \"+spp\" argument\n      to `--erl`.\n\n  ## Examples\n\n  Run a command:\n\n  ```elixir\n  iex> MuonTrap.cmd(\"echo\", [\"hello\"])\n  {\"hello\\n\", 0}\n  ```\n\n  The next examples only run on Linux. To try this out, create new cgroups:\n\n  ```sh\n  sudo cgcreate -a $(whoami) -g memory,cpu:muontrap\n  ```\n\n  Run a command, but limit memory so severely that it doesn't work (for demo\n  purposes, obviously):\n\n  ```elixir\n  iex-donttest> MuonTrap.cmd(\"echo\", [\"hello\"], cgroup_controllers: [\"memory\"], cgroup_path: \"muontrap/test\", cgroup_sets: [{\"memory\", \"memory.limit_in_bytes\", \"8192\"}])\n  {\"\", 1}\n  ```\n\n  Run a command with a timeout:\n\n  iex> MuonTrap.cmd(\"/bin/sh\", [\"-c\", \"echo start && sleep 10 && echo end\"], timeout: 100)\n  {\"start\\n\", :timeout}\n  \"\"\"\n  @spec cmd(binary(), [binary()], keyword()) ::\n          {Collectable.t(), exit_status :: non_neg_integer() | :timeout}\n  def cmd(command, args, opts \\\\ []) when is_binary(command) and is_list(args) do\n    options = MuonTrap.Options.validate(:cmd, command, args, opts)\n\n    MuonTrap.Port.cmd(options)\n  end\n\n  @doc \"\"\"\n  Return the absolute path to the muontrap executable.\n\n  Call this if you want to invoke the `muontrap` port binary manually.\n  \"\"\"\n  defdelegate muontrap_path, to: MuonTrap.Port\nend\n"
  },
  {
    "path": "mix.exs",
    "content": "defmodule MuonTrap.MixProject do\n  use Mix.Project\n\n  @version \"1.7.0\"\n  @source_url \"https://github.com/fhunleth/muontrap\"\n\n  def project do\n    [\n      app: :muontrap,\n      version: @version,\n      elixir: \"~> 1.11\",\n      description: \"Keep your ports contained\",\n      source_url: @source_url,\n      elixirc_paths: elixirc_paths(Mix.env()),\n      docs: docs(),\n      start_permanent: Mix.env() == :prod,\n      deps: deps(),\n      compilers: [:elixir_make | Mix.compilers()],\n      make_targets: [\"all\"],\n      make_clean: [\"clean\"],\n      dialyzer: [\n        flags: [:missing_return, :extra_return, :unmatched_returns, :error_handling, :underspecs]\n      ],\n      package: package()\n    ]\n  end\n\n  defp elixirc_paths(:test), do: [\"lib\", \"test/support\"]\n  defp elixirc_paths(_), do: [\"lib\"]\n\n  def application do\n    [extra_applications: [:logger]]\n  end\n\n  def cli do\n    [preferred_envs: %{docs: :docs, \"hex.publish\": :docs, \"hex.build\": :docs}]\n  end\n\n  defp deps() do\n    [\n      {:elixir_make, \"~> 0.6\", runtime: false},\n      {:ex_doc, \"~> 0.19\", only: :docs, runtime: false},\n      {:dialyxir, \"~> 1.2\", only: :dev, runtime: false},\n      {:credo, \"~> 1.5\", only: :dev, runtime: false}\n    ]\n  end\n\n  defp docs do\n    [\n      extras: [\"README.md\"],\n      main: \"readme\",\n      source_ref: \"v#{@version}\",\n      source_url: @source_url\n    ]\n  end\n\n  defp package() do\n    [\n      files: [\n        \"CHANGELOG.md\",\n        \"README.md\",\n        \"lib\",\n        \"c_src/*.[ch]\",\n        \"c_src/Makefile\",\n        \"Makefile\",\n        \"mix.exs\",\n        \"NOTICE\",\n        \"LICENSES/*\",\n        \"REUSE.toml\"\n      ],\n      licenses: [\"Apache-2.0\"],\n      links: %{\n        \"Changelog\" => \"#{@source_url}/blob/main/CHANGELOG.md\",\n        \"GitHub\" => @source_url,\n        \"REUSE Compliance\" => \"https://api.reuse.software/info/github.com/fhunleth/muontrap\"\n      }\n    ]\n  end\nend\n"
  },
  {
    "path": "test/Makefile",
    "content": "# Variables to override\n#\n# CC            C compiler\n# CROSSCOMPILE\tcrosscompiler prefix, if any\n# CFLAGS\tcompiler flags for compiling all C files\n# LDFLAGS\tlinker flags for linking all binaries\n\nLDFLAGS +=\nCFLAGS ?= -O2 -Wall -Wextra -Wno-unused-parameter\nCFLAGS += -std=c99 -D_GNU_SOURCE\n\nSRC=$(wildcard *.c)\nBIN=$(SRC:.c=.test)\n\n.PHONY: all clean\n\nall: $(BIN)\n\n%.test: %.c\n\t$(CC) $(LDFLAGS) $(CFLAGS) -o $@ $<\n\nclean:\n\trm -f *.test\n"
  },
  {
    "path": "test/cgroup_test.exs",
    "content": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n#\n# SPDX-License-Identifier: Apache-2.0\n\ndefmodule CgroupTest do\n  use MuonTrapTest.Case\n\n  alias MuonTrap.Cgroups\n\n  @tag :cgroup\n  test \"test environment cgroup support enabled\" do\n    assert Cgroups.cgroups_enabled?()\n\n    {:ok, controllers} = Cgroups.get_controllers()\n    # cpu and memory controllers need to be enabled for the unit tests\n    assert \"cpu\" in controllers\n    assert \"memory\" in controllers\n  end\n\n  @tag :cgroup\n  test \"cgroup gets created and removed on exit\" do\n    cgroup_path = random_cgroup_path()\n\n    port =\n      Port.open(\n        {:spawn_executable, MuonTrap.muontrap_path()},\n        args: [\"-g\", cgroup_path, \"-c\", \"cpu\", \"./test/do_nothing.test\"]\n      )\n\n    os_pid = os_pid(port)\n    assert_os_pid_running(os_pid)\n    assert cpu_cgroup_exists(cgroup_path)\n\n    Port.close(port)\n\n    wait_for_close_check()\n    assert_os_pid_exited(os_pid)\n    assert !cpu_cgroup_exists(cgroup_path)\n  end\n\n  @tag :cgroup\n  test \"cleans up after a forking process\" do\n    cgroup_path = random_cgroup_path()\n\n    port =\n      Port.open(\n        {:spawn_executable, MuonTrap.muontrap_path()},\n        args: [\"-g\", cgroup_path, \"-c\", \"cpu\", \"./test/fork_a_lot.test\"]\n      )\n\n    os_pid = os_pid(port)\n    assert_os_pid_running(os_pid)\n    assert cpu_cgroup_exists(cgroup_path)\n\n    Port.close(port)\n\n    wait_for_close_check()\n    assert_os_pid_exited(os_pid)\n    assert !cpu_cgroup_exists(cgroup_path)\n  end\n\n  @tag :cgroup\n  test \"get and set cgroup variables\" do\n    cgroup_path = random_cgroup_path()\n\n    port =\n      Port.open(\n        {:spawn_executable, MuonTrap.muontrap_path()},\n        args: [\"-g\", cgroup_path, \"-c\", \"memory\", \"./test/do_nothing.test\"]\n      )\n\n    os_pid = os_pid(port)\n    assert_os_pid_running(os_pid)\n    assert memory_cgroup_exists(cgroup_path)\n\n    {:ok, memory_str} = Cgroups.cgget(\"memory\", cgroup_path, \"memory.limit_in_bytes\")\n    {memory, _} = Integer.parse(memory_str)\n    assert memory > 1000\n\n    # :ok = Cgroups.cgset(\"memory\", cgroup_path, \"memory.limit_in_bytes\", \"900\")\n\n    Port.close(port)\n  end\nend\n"
  },
  {
    "path": "test/chatty.c",
    "content": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n// SPDX-FileCopyrightText: 2023 Ben Youngblood\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <stdio.h>\n\nint main(void)\n{\n  /* Make standard output unbuffered. */\n  setvbuf(stdout, (char *)NULL, _IONBF, 0);\n\n  while (1)\n    printf(\"Hello, world!\\n\");\n\n  return 0;\n}\n"
  },
  {
    "path": "test/daemon_test.exs",
    "content": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2018 Matt Ludwigs\n# SPDX-FileCopyrightText: 2019 Timmo Verlaan\n# SPDX-FileCopyrightText: 2022 Gustavo Brunoro\n# SPDX-FileCopyrightText: 2023 Ben Youngblood\n# SPDX-FileCopyrightText: 2023 Eric Rauer\n# SPDX-FileCopyrightText: 2023 Jon Carstens\n# SPDX-FileCopyrightText: 2025 Fernando Mumbach\n#\n# SPDX-License-Identifier: Apache-2.0\n\ndefmodule DaemonTest do\n  use MuonTrapTest.Case\n  import ExUnit.CaptureLog\n  import ExUnit.CaptureIO\n\n  alias MuonTrap.Daemon\n\n  defp daemon_spec(cmd, args) do\n    Supervisor.child_spec({Daemon, [cmd, args]}, id: :test_daemon)\n  end\n\n  defp daemon_spec(cmd, args, opts) do\n    Supervisor.child_spec({Daemon, [cmd, args, opts]}, id: :test_daemon)\n  end\n\n  test \"stopping the daemon kills the process\" do\n    {:ok, pid} = start_supervised(daemon_spec(test_path(\"do_nothing.test\"), []))\n\n    os_pid = Daemon.os_pid(pid)\n    assert_os_pid_running(os_pid)\n\n    :ok = stop_supervised(:test_daemon)\n\n    wait_for_close_check()\n    assert_os_pid_exited(os_pid)\n  end\n\n  test \"stopping the daemon kill very chatty processes\" do\n    fun = fn ->\n      # Try up to 5 times to avoid false negatives. If the error is present, the\n      # test will nearly always fail on the first iteration.\n      for _ <- 1..5 do\n        {:ok, pid} =\n          start_supervised(daemon_spec(test_path(\"chatty.test\"), [], log_output: :debug))\n\n        os_pid = Daemon.os_pid(pid)\n        assert_os_pid_running(os_pid)\n\n        child_pid = find_child_pid(os_pid)\n        assert is_integer(child_pid)\n\n        :ok = stop_supervised(:test_daemon)\n\n        wait_for_close_check()\n        assert_os_pid_exited(os_pid)\n\n        if os_pid_around?(child_pid) do\n          System.cmd(\"kill\", [\"-9\", \"#{child_pid}\"])\n          flunk(\"muontrap process exited but child process was still running\")\n        end\n      end\n    end\n\n    # For this test, it's critical to capture the log output even though we don't\n    # use it; not doing so significantly increases the likelihood of false\n    # negatives.\n    capture_log([level: :info], fun)\n  end\n\n  @spec find_child_pid(non_neg_integer()) :: non_neg_integer() | nil\n  def find_child_pid(os_pid) do\n    {output, _} = System.cmd(\"ps\", [\"-eo\", \"ppid,pid\"])\n\n    output\n    |> String.split(\"\\n\")\n    |> Enum.find_value(fn line ->\n      parsed_line = line |> String.trim() |> String.split(~r/\\s+/)\n\n      with [ppid, pid] <- parsed_line,\n           true <- ppid == to_string(os_pid),\n           {pid, \"\"} <- Integer.parse(pid) do\n        pid\n      else\n        _ -> nil\n      end\n    end)\n  end\n\n  test \"daemon logs output when told\" do\n    fun = fn ->\n      {:ok, _pid} = start_supervised(daemon_spec(\"echo\", [\"hello\"], log_output: :error))\n\n      wait_for_close_check()\n      Logger.flush()\n    end\n\n    assert capture_log(fun) =~ \"hello\"\n  end\n\n  test \"daemon logs are passed through log_transform fn\" do\n    fun = fn ->\n      {:ok, _pid} =\n        start_supervised(\n          daemon_spec(\n            \"echo\",\n            [\"hello\"],\n            log_output: :error,\n            log_transform: &String.replace(&1, \"hello\", \"goodbye\")\n          )\n        )\n\n      wait_for_close_check()\n      Logger.flush()\n    end\n\n    assert capture_log(fun) =~ \"goodbye\"\n  end\n\n  test \"daemon doesn't log output by default\" do\n    fun = fn ->\n      {:ok, _pid} =\n        start_supervised(daemon_spec(test_path(\"echo_stdio.test\"), [], stderr_to_stdout: true))\n\n      wait_for_close_check()\n\n      Logger.flush()\n    end\n\n    assert capture_log(fun) == \"\"\n  end\n\n  test \"daemon logs output to stderr when told\" do\n    fun = fn ->\n      {:ok, pid} =\n        start_supervised(\n          daemon_spec(test_path(\"echo_stderr.test\"), [],\n            log_output: :error,\n            stderr_to_stdout: true\n          )\n        )\n\n      wait_for_output(pid, 15, 500)\n      Logger.flush()\n    end\n\n    assert capture_log(fun) =~ \"stderr message\"\n  end\n\n  test \"daemon does not log output to stderr when not told\" do\n    # Need to disable ANSI since new line in log message is important\n    Application.put_env(:elixir, :ansi_enabled, false)\n\n    fun = fn ->\n      {:ok, pid} =\n        start_supervised(\n          daemon_spec(test_path(\"echo_stdio.test\"), [],\n            log_output: :error,\n            stderr_to_stdout: false\n          )\n        )\n\n      wait_for_output(pid, 12, 500)\n\n      Logger.flush()\n    end\n\n    result = capture_log(fun)\n    assert result =~ \"echo_stdio.test: stdout here\\n\"\n    refute result =~ \"..\"\n\n    Application.delete_env(:elixir, :ansi_enabled)\n  end\n\n  test \"daemon logs to a custom prefix\" do\n    fun = fn ->\n      {:ok, _pid} =\n        start_supervised(\n          daemon_spec(\"echo\", [\"hello\"], log_output: :error, log_prefix: \"echo says: \")\n        )\n\n      wait_for_close_check()\n      Logger.flush()\n    end\n\n    assert capture_log(fun) =~ \"echo says: hello\"\n  end\n\n  test \"daemon logs include metadata\" do\n    fun = fn ->\n      {:ok, _pid} =\n        start_supervised(\n          daemon_spec(\n            \"echo\",\n            [\"-n\", \"hello\"],\n            log_output: :error,\n            logger_metadata: [foo: :bar]\n          )\n        )\n\n      wait_for_close_check()\n      Logger.flush()\n    end\n\n    logger_opts = [\n      metadata: [:foo, :muontrap_cmd, :muontrap_args],\n      format: \"[$level] $message $metadata\\n\"\n    ]\n\n    log_output = capture_log(logger_opts, fun)\n    assert log_output =~ \"foo=bar\"\n    assert log_output =~ \"muontrap_cmd=echo\"\n    assert log_output =~ \"muontrap_args=-n hello\"\n  end\n\n  test \"daemon supports custom logger (captured function)\" do\n    test_process = self()\n\n    logger = fn line ->\n      send(test_process, line)\n    end\n\n    fun = fn ->\n      {:ok, pid} =\n        start_supervised(\n          daemon_spec(test_path(\"echo_stdio.test\"), [],\n            log_output: :error,\n            logger_fun: logger,\n            stderr_to_stdout: false\n          )\n        )\n\n      wait_for_output(pid, 12, 500)\n\n      Logger.flush()\n    end\n\n    log_output = capture_log(fun)\n\n    refute log_output =~ \"stdout here\"\n\n    assert_receive \"stdout here\", 500\n    refute_receive _\n  end\n\n  test \"daemon supports custom logger (mfa)\" do\n    fun = fn ->\n      {:ok, pid} =\n        start_supervised(\n          daemon_spec(test_path(\"echo_stdio.test\"), [],\n            log_output: :error,\n            logger_fun: {__MODULE__, :logger_fun_fun},\n            stderr_to_stdout: false\n          )\n        )\n\n      wait_for_output(pid, 12, 500)\n\n      Logger.flush()\n    end\n\n    log_output = capture_log(fun)\n\n    assert log_output =~ \"stdout here\"\n    refute log_output =~ \"logger_fun\"\n\n    stop_supervised(:test_daemon)\n\n    fun = fn ->\n      {:ok, pid} =\n        start_supervised(\n          daemon_spec(test_path(\"echo_stdio.test\"), [],\n            log_output: :error,\n            logger_fun: {__MODULE__, :logger_fun_fun, [\"logger_fun: \"]},\n            stderr_to_stdout: false\n          )\n        )\n\n      wait_for_output(pid, 12, 500)\n\n      Logger.flush()\n    end\n\n    log_output = capture_log(fun)\n\n    assert log_output =~ \"logger_fun: stdout here\"\n  end\n\n  @spec logger_fun_fun(binary(), binary()) :: :ok\n  def logger_fun_fun(line, prefix \\\\ \"\") do\n    require Logger\n    Logger.info([prefix, line])\n  end\n\n  defp wait_for_output(_pid, count, time_left) when time_left <= 0 do\n    flunk(\"Didn't get #{count} output bytes from daemon process in time\")\n  end\n\n  defp wait_for_output(pid, count, time_left) do\n    got = Daemon.statistics(pid).output_byte_count\n\n    cond do\n      got < count ->\n        Process.sleep(100)\n        wait_for_output(pid, count, time_left - 100)\n\n      got > count ->\n        flunk(\"Got too much output: #{got}, but expected #{count}\")\n\n      true ->\n        :ok\n    end\n  end\n\n  test \"can pass environment variables to the daemon\" do\n    fun = fn ->\n      {:ok, _pid} =\n        start_supervised(\n          daemon_spec(\n            \"env\",\n            [],\n            log_output: :error,\n            stderr_to_stdout: true,\n            env: [{\"MUONTRAP_TEST_VAR\", \"HELLO_THERE\"}]\n          )\n        )\n\n      wait_for_close_check()\n\n      Logger.flush()\n    end\n\n    assert capture_log(fun) =~ \"MUONTRAP_TEST_VAR=HELLO_THERE\"\n  end\n\n  test \"transient daemon restarts on errored exits\" do\n    # :transient means that successful exits don't restart, but\n    # failed exits do.\n\n    tempfile = Path.join(\"test\", \"tmp-transient_daemon\")\n    _ = File.rm(tempfile)\n\n    log =\n      capture_log(fn ->\n        {:ok, _pid} =\n          start_supervised(\n            {Daemon, [test_path(\"succeed_second_time.test\"), [tempfile], [log_output: :error]]},\n            restart: :transient\n          )\n\n        # Give it time to run twice if successful or more than twice if not.\n        Process.sleep(500)\n\n        Logger.flush()\n      end)\n\n    _ = File.rm(tempfile)\n\n    assert log =~ \"Called 0 times\"\n    assert log =~ \"Called 1 times\"\n    refute log =~ \"Called 2 times\"\n  end\n\n  test \"permanent daemon always restarts\" do\n    tempfile = Path.join(\"test\", \"tmp-permanent_deamon\")\n    _ = File.rm(tempfile)\n\n    log =\n      capture_log(fn ->\n        {:ok, _pid} =\n          start_supervised(\n            Supervisor.child_spec(\n              {Daemon, [test_path(\"succeed_second_time.test\"), [tempfile], [log_output: :error]]},\n              restart: :permanent,\n              id: :test_daemon\n            )\n          )\n\n        # Give it time to restart a few times.\n        Process.sleep(500)\n\n        stop_supervised(:test_daemon)\n\n        Logger.flush()\n      end)\n\n    _ = File.rm(tempfile)\n\n    assert log =~ \"Called 0 times\"\n    assert log =~ \"Called 1 times\"\n    assert log =~ \"Called 2 times\"\n  end\n\n  test \"returns :error_exit_status for stop reason\" do\n    {:ok, pid} = start_supervised(daemon_spec(test_path(\"kill_self_with_sigusr1.test\"), []))\n\n    ref = Process.monitor(pid)\n\n    os_pid = Daemon.os_pid(pid)\n\n    assert_receive {:DOWN, ^ref, :process, _object, :error_exit_status}\n    assert_os_pid_exited(os_pid)\n\n    :ok = stop_supervised(:test_daemon)\n\n    wait_for_close_check()\n  end\n\n  test \"supports mapping exit status to stop reason\" do\n    # Some systems may have SIGUSR1 == 10 and others\n    # SIGUSR1 == 30. Do a quick lookup for the expected\n    # signal mapping to decide which one to expect\n    sigusr1 = s2n(\"USR1\", 10)\n\n    {:ok, pid} =\n      start_supervised(\n        daemon_spec(test_path(\"kill_self_with_sigusr1.test\"), [],\n          exit_status_to_reason: fn s ->\n            if s == 128 + sigusr1 do\n              :error_exit_sigusr1\n            else\n              {:error_exit_status, s}\n            end\n          end\n        )\n      )\n\n    ref = Process.monitor(pid)\n\n    os_pid = Daemon.os_pid(pid)\n\n    assert_receive {:DOWN, ^ref, :process, _object, :error_exit_sigusr1}\n    assert_os_pid_exited(os_pid)\n\n    :ok = stop_supervised(:test_daemon)\n\n    wait_for_close_check()\n  end\n\n  defp s2n(name, default) do\n    with :error <- s2n_kill_l_name(name),\n         :error <- s2n_kill_l(name) do\n      default\n    end\n  end\n\n  defp s2n_kill_l_name(name) do\n    with {results, 0} <- System.cmd(\"kill\", [\"-l\", name], stderr_to_stdout: true),\n         {number, _} <- Integer.parse(results),\n         true <- is_integer(number) do\n      number\n    else\n      _ -> :error\n    end\n  end\n\n  defp s2n_kill_l(name) do\n    # Parse the result from MacOS kill.\n    #\n    # There are many formats for `kill -l` and this only supports the one on\n    # MacOS that we're getting.\n    case System.cmd(\"kill\", [\"-l\"], stderr_to_stdout: true) do\n      {signals, 0} ->\n        String.split(signals)\n        |> Enum.with_index(1)\n        |> List.keyfind(name, 0, {:hack, :error})\n        |> elem(1)\n\n      _ ->\n        :error\n    end\n  end\n\n  @tag :cgroup\n  test \"can start daemon with cgroups\" do\n    {:ok, pid} =\n      start_supervised(\n        daemon_spec(\n          test_path(\"do_nothing.test\"),\n          [],\n          cgroup_base: \"muontrap_test\",\n          cgroup_controllers: [\"memory\"]\n        )\n      )\n\n    os_pid = Daemon.os_pid(pid)\n    assert_os_pid_running(os_pid)\n\n    {:ok, memory_str} = Daemon.cgget(pid, \"memory\", \"memory.limit_in_bytes\")\n    {memory, _} = Integer.parse(memory_str)\n    assert memory > 1000\n  end\n\n  test \"flow control when logging\" do\n    fun = fn ->\n      {:ok, _pid} =\n        start_supervised(\n          daemon_spec(test_path(\"print_a_lot.test\"), [],\n            log_output: :error,\n            stdio_window: 101\n          )\n        )\n\n      wait_for_close_check(200)\n      Logger.flush()\n    end\n\n    results = capture_log(fun)\n\n    split =\n      String.split(results, \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\")\n\n    # Check that we have a log message for all 1000 lines plus the leftovers at the end.\n    assert length(split) == 1001\n  end\n\n  test \"line splits on newlines\" do\n    # Daemon.process_data(data) :: {lines, leftovers}\n    assert {[], \"abcd\"} == Daemon.process_data(\"abcd\")\n    assert {[\"abcd\"], \"\"} == Daemon.process_data(\"abcd\\n\")\n    assert {[\"abcd\", \"\"], \"\"} == Daemon.process_data(\"abcd\\n\\n\")\n    assert {[\"\"], \"abcd\"} == Daemon.process_data(\"\\nabcd\")\n    assert {[\"abcd\"], \"\"} == Daemon.process_data(\"abcd\\n\")\n    assert {[\"a\", \"b\", \"c\", \"d\"], \"\"} == Daemon.process_data(\"a\\nb\\nc\\nd\\n\")\n  end\n\n  test \"line splits trim max amount to buffer\" do\n    a255 = :binary.copy(\"a\", 255)\n    a256 = :binary.copy(\"a\", 256)\n    a265 = :binary.copy(\"a\", 265)\n\n    # Trims amount to buffer when no newlines\n    assert {[], a256} == Daemon.process_data(a265)\n\n    # Doesn't trim if not needed\n    assert {[], a255} == Daemon.process_data(a255)\n\n    # Doesn't trim full lines if complete\n    assert {[a265, \"abcd\"], \"ef\"} == Daemon.process_data(a265 <> \"\\nabcd\\nef\")\n\n    # Trims leftovers and returns lines\n    assert {[\"abc\"], a256} == Daemon.process_data(\"abc\\n\" <> a265)\n  end\n\n  test \"daemon inspects non-utf8 strings\" do\n    output =\n      capture_io(:user, fn ->\n        {:ok, pid} =\n          start_supervised(daemon_spec(test_path(\"echo_junk.test\"), [], log_output: :error))\n\n        wait_for_output(pid, 15, 500)\n        Logger.flush()\n      end)\n\n    refute output =~ \"FORMATTER ERROR: bad return value\"\n    refute output =~ \"** (RuntimeError) bad return value from Logger formatter Logger.Formatter\"\n\n    if Version.match?(System.version(), \">= 1.16.0\") do\n      assert output =~ \"��ti�g!c\"\n    else\n      assert output =~ \"** MuonTrap filtered 14 non-UTF8 bytes **\"\n    end\n  end\n\n  test \"daemon captures only stderr when capture_stderr_only is set\" do\n    fun = fn ->\n      {:ok, pid} =\n        start_supervised(\n          daemon_spec(test_path(\"echo_both.test\"), [],\n            log_output: :error,\n            capture_stderr_only: true\n          )\n        )\n\n      wait_for_output(pid, 15, 500)\n      Logger.flush()\n    end\n\n    log = capture_log(fun)\n    assert log =~ \"stderr message\"\n    refute log =~ \"stdout message\"\n  end\n\n  test \"daemon captures stderr only without log_output (no crash)\" do\n    {:ok, pid} =\n      start_supervised(\n        daemon_spec(test_path(\"echo_stderr.test\"), [],\n          # no log_output & no logger_fun, so no logging\n          # even if capture_stderr_only is true\n          capture_stderr_only: true\n        )\n      )\n\n    os_pid = Daemon.os_pid(pid)\n    assert_os_pid_running(os_pid)\n\n    wait_for_output(pid, 15, 500)\n\n    :ok = stop_supervised(:test_daemon)\n\n    wait_for_close_check()\n    assert_os_pid_exited(os_pid)\n  end\n\n  test \"daemon captures both stdout and stderr when both options are used\" do\n    fun = fn ->\n      {:ok, pid} =\n        start_supervised(\n          daemon_spec(test_path(\"echo_both.test\"), [],\n            log_output: :error,\n            stderr_to_stdout: true\n          )\n        )\n\n      wait_for_output(pid, 30, 500)\n      Logger.flush()\n    end\n\n    log = capture_log(fun)\n    assert log =~ \"stderr message\"\n    assert log =~ \"stdout message\"\n  end\n\n  test \"daemon captures only stdout when stderr_to_stdout is false\" do\n    fun = fn ->\n      {:ok, pid} =\n        start_supervised(\n          daemon_spec(test_path(\"echo_both.test\"), [],\n            log_output: :error,\n            stderr_to_stdout: false\n          )\n        )\n\n      wait_for_output(pid, 15, 500)\n      Logger.flush()\n    end\n\n    log = capture_log(fun)\n    refute log =~ \"stderr message\"\n    assert log =~ \"stdout message\"\n  end\nend\n"
  },
  {
    "path": "test/do_nothing.c",
    "content": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <stdio.h>\n#include <stdlib.h>\n#include <unistd.h>\n\nint main(int argc, char **argv)\n{\n    // Hang out long enough to satisfy the tests\n    sleep(120);\n    exit(0);\n}\n"
  },
  {
    "path": "test/echo_both.c",
    "content": "// SPDX-FileCopyrightText: 2024 Frank Hunleth\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <stdio.h>\n#include <stdlib.h>\n#include <unistd.h>\n\nint main()\n{\n    fprintf(stdout, \"stdout message\\n\");\n    fflush(stdout);\n    fprintf(stderr, \"stderr message\\n\");\n    fflush(stderr);\n\n    // Hang out long enough to satisfy the tests\n    sleep(120);\n    exit(0);\n}\n"
  },
  {
    "path": "test/echo_junk.c",
    "content": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <stdio.h>\n#include <stdlib.h>\n#include <unistd.h>\n\nint main()\n{\n    const char junk[] = {253, 245, 116, 105, 238, 103, 33, 99, 235, 229, 124, 121, 255, 229, 10};\n\n    fwrite(junk, sizeof(junk), 1, stdout);\n    fflush(stdout);\n\n    // Hang out long enough to satisfy the tests\n    sleep(200);\n    exit(0);\n}\n"
  },
  {
    "path": "test/echo_stderr.c",
    "content": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n// SPDX-FileCopyrightText: 2019 Timmo Verlaan\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <stdio.h>\n#include <stdlib.h>\n#include <unistd.h>\n\nint main()\n{\n    fprintf(stderr, \"stderr message\\n\");\n    // Hang out long enough to satisfy the tests\n    sleep(120);\n    exit(0);\n}\n"
  },
  {
    "path": "test/echo_stdio.c",
    "content": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <stdio.h>\n#include <stdlib.h>\n#include <unistd.h>\n\nint main()\n{\n    // Messages are different lengths on purpose to help debug.\n    // stderr is dots to make it less ugly when it prints to the console, but\n    // I'll probably forget and regret it.\n    fprintf(stdout, \"stdout here\\n\");\n    fprintf(stderr, \"....\");\n    fflush(stdout);\n\n    // Hang out long enough to satisfy the tests\n    sleep(200);\n    exit(0);\n}\n"
  },
  {
    "path": "test/fork_a_lot.c",
    "content": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <signal.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <sys/types.h>\n#include <unistd.h>\n\n// Fork a tree of children and print out the pids\n\nstatic void do_fork(int left)\n{\n    if (left == 0)\n        return;\n\n    for (int i = 0; i < 2; i++) {\n        pid_t pid = fork();\n        if (pid == 0) {\n            // Child\n            do_fork(left - 1);\n            // Hang out long enough to satisfy the tests\n            sleep(120);\n        }\n        printf(\"%d\\n\", pid);\n        fflush(stdout);\n    }\n\n}\nint main(int argc, char **argv)\n{\n    // Fork a tree of children.\n    // 4 -> this pid + 2 children + 4 grandchildren + 8 great-grandchildren, etc.\n    // for a total of 2^(4+1) - 1 processes\n    do_fork(4);\n\n    // parent\n    sleep(120);\n    exit(0);\n}\n"
  },
  {
    "path": "test/ignore_sigterm.c",
    "content": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <signal.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <unistd.h>\n\nint main(int argc, char **argv)\n{\n    sigset_t mask;\n    sigemptyset(&mask);\n    sigaddset(&mask, SIGTERM);\n    sigprocmask(SIG_BLOCK, &mask, NULL);\n\n    sleep(120);\n    exit(0);\n}\n"
  },
  {
    "path": "test/kill_self_with_signal.c",
    "content": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <err.h>\n#include <signal.h>\n#include <stdlib.h>\n#include <unistd.h>\n\nint main(int argc, char **argv)\n{\n    // This test kills itself with a SIGTERM to see if\n    // muontrap reports the expected exit code.\n    if (kill(getpid(), SIGTERM) < 0)\n        err(EXIT_FAILURE, \"kill\");\n\n    // Give the OS up to a second to deliver the signal.\n    sleep(1);\n\n    errx(EXIT_FAILURE, \"expected a signal\");\n}\n"
  },
  {
    "path": "test/kill_self_with_sigusr1.c",
    "content": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n// SPDX-FileCopyrightText: 2023 Eric Rauer\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <err.h>\n#include <signal.h>\n#include <stdlib.h>\n#include <unistd.h>\n\nint main(int argc, char **argv)\n{\n    // This test kills itself with a SIGUSR1 to see if\n    // muontrap reports the expected exit code.\n    if (kill(getpid(), SIGUSR1) < 0)\n        err(EXIT_FAILURE, \"kill\");\n\n    // Give the OS up to a second to deliver the signal.\n    sleep(1);\n\n    errx(EXIT_FAILURE, \"expected a signal\");\n}\n\n"
  },
  {
    "path": "test/muontrap_test.exs",
    "content": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2019 Jason Axelson\n# SPDX-FileCopyrightText: 2023 Ben Youngblood\n# SPDX-FileCopyrightText: 2023 Jon Carstens\n#\n# SPDX-License-Identifier: Apache-2.0\n\ndefmodule MuonTrapTest do\n  use MuonTrapTest.Case\n\n  doctest MuonTrap\n\n  defp run_muontrap(args) do\n    # Directly invoke the muontrap port to reduce the amount of code\n    # to debug if something breaks.\n    port =\n      Port.open(\n        {:spawn_executable, MuonTrap.muontrap_path()},\n        args: args\n      )\n\n    # The port starts asynchronously. If the test needs to register\n    # a signal handler, this is problematic since we can beat it.\n    # The right answer is to handshake with our test helper app.\n    # Since that's work, sleep briefly.\n    Process.sleep(10)\n    port\n  end\n\n  test \"closing the port kills the process\" do\n    port = run_muontrap([\"./test/do_nothing.test\"])\n\n    os_pid = os_pid(port)\n    assert_os_pid_running(os_pid)\n\n    Port.close(port)\n\n    wait_for_close_check()\n    assert_os_pid_exited(os_pid)\n  end\n\n  test \"closing the port kills a process that ignores sigterm\" do\n    port = run_muontrap([\"--delay-to-sigkill\", \"1\", \"test/ignore_sigterm.test\"])\n\n    os_pid = os_pid(port)\n    assert_os_pid_running(os_pid)\n    Port.close(port)\n\n    wait_for_close_check()\n    assert_os_pid_exited(os_pid)\n  end\n\n  test \"delaying the SIGKILL\" do\n    port = run_muontrap([\"--delay-to-sigkill\", \"250\", \"test/ignore_sigterm.test\"])\n\n    Process.sleep(10)\n    os_pid = os_pid(port)\n    assert_os_pid_running(os_pid)\n    Port.close(port)\n\n    Process.sleep(100)\n    # process should be around for 250ms, so it should be around here.\n    assert_os_pid_running(os_pid)\n\n    Process.sleep(200)\n\n    # Now it should be gone\n    assert_os_pid_exited(os_pid)\n  end\n\n  # The following tests are copied from System.cmd to help ensure that\n  # MuonTrap.cmd/3 works similarly.\n  test \"cmd/2 raises for null bytes\" do\n    assert_raise ArgumentError,\n                 ~r\"cannot execute MuonTrap.cmd/3 for program with null byte\",\n                 fn ->\n                   MuonTrap.cmd(\"null\\0byte\", [])\n                 end\n  end\n\n  test \"cmd/3 raises with non-binary arguments\" do\n    assert_raise ArgumentError, ~r\"all arguments for MuonTrap.cmd/3 must be binaries\", fn ->\n      MuonTrap.cmd(\"ls\", [~c\"/usr\"])\n    end\n  end\n\n  test \"cmd/2\" do\n    assert {\"hello\\n\", 0} = MuonTrap.cmd(\"echo\", [\"hello\"])\n  end\n\n  test \"cmd/3 (with options)\" do\n    opts = [\n      into: [],\n      cd: File.cwd!(),\n      env: %{\"foo\" => \"bar\", \"baz\" => nil},\n      arg0: \"echo\",\n      stderr_to_stdout: true,\n      parallelism: true\n    ]\n\n    assert {[\"hello\\n\"], 0} = MuonTrap.cmd(\"echo\", [\"hello\"], opts)\n  end\n\n  test \"cmd/3 that prints a lot w/ default buffer\" do\n    opts = [\n      parallelism: true\n    ]\n\n    {output, 0} = MuonTrap.cmd(test_path(\"print_a_lot.test\"), [], opts)\n    split = String.split(output, \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\")\n    assert length(split) == 1001\n  end\n\n  test \"cmd/3 that prints a lot w/ smaller buffer\" do\n    opts = [\n      parallelism: true,\n      stdio_window: 63\n    ]\n\n    {output, 0} = MuonTrap.cmd(test_path(\"print_a_lot.test\"), [], opts)\n    split = String.split(output, \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\")\n    assert length(split) == 1001\n  end\n\n  test \"cmd/3 with timeout\" do\n    opts = [timeout: 250]\n\n    fun = fn ->\n      MuonTrap.cmd(test_path(\"chatty.test\"), [], opts)\n    end\n\n    assert {elapsed, {output, :timeout}} = :timer.tc(fun)\n    elapsed = div(elapsed, 1000)\n\n    assert_in_delta opts[:timeout], elapsed, 50\n    assert byte_size(output) > 0\n  end\n\n  test \"cmd/3 with timeout cleans up timers\" do\n    opts = [timeout: 100]\n\n    {_, status} = MuonTrap.cmd(test_path(\"kill_self_with_signal.test\"), [], opts)\n\n    refute status == :timeout\n    refute_receive :timeout\n  end\n\n  test \"cmd/3 doesn't eat any messages sent to the process\" do\n    opts = [timeout: 250]\n\n    message = {:timeout, make_ref()}\n    Process.send_after(self(), message, 10)\n    Process.send_after(self(), :foo, 10)\n\n    assert {_, :timeout} = MuonTrap.cmd(test_path(\"do_nothing.test\"), [], opts)\n\n    assert_receive ^message\n    assert_receive :foo\n  end\n\n  # Test adapted from https://github.com/elixir-lang/elixir/blob/v1.15.0/lib/elixir/test/elixir/system_test.exs#L121\n  @echo \"echo-elixir-test\"\n  @tag :tmp_dir\n  test \"cmd/2 with absolute and relative paths\", config do\n    echo = Path.join(config.tmp_dir, @echo)\n    File.mkdir_p!(Path.dirname(echo))\n    File.ln_s!(System.find_executable(\"echo\"), echo)\n\n    File.cd!(Path.dirname(echo), fn ->\n      # There is a bug in OTP where find_executable is finding\n      # entries on the current directory. If this is the case,\n      # we should avoid the assertion below.\n      if !System.find_executable(@echo) do\n        assert :enoent = catch_error(MuonTrap.cmd(@echo, [\"hello\"]))\n      end\n\n      assert {\"hello\\n\", 0} =\n               MuonTrap.cmd(Path.join(File.cwd!(), @echo), [\"hello\"], [{:arg0, \"echo\"}])\n    end)\n  end\n\n  test \"signals return an exit code of 128 + signal\" do\n    # SIGTERM == 15\n    assert {\"\", 128 + 15} == MuonTrap.cmd(test_path(\"kill_self_with_signal.test\"), [])\n  end\n\n  test \"README.md version is up to date\" do\n    app = :muontrap\n    app_version = Application.spec(app, :vsn) |> to_string()\n    readme = File.read!(\"README.md\")\n    [_, readme_version] = Regex.run(~r/{:#{app}, \"(.+)\"}/, readme)\n    assert Version.match?(app_version, readme_version)\n  end\nend\n"
  },
  {
    "path": "test/options_test.exs",
    "content": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2023 Ben Youngblood\n# SPDX-FileCopyrightText: 2023 Jon Carstens\n#\n# SPDX-License-Identifier: Apache-2.0\n\ndefmodule MuonTrap.OptionsTest do\n  use MuonTrapTest.Case\n\n  alias MuonTrap.Options\n\n  test \"creates random cgroup path when asked\" do\n    options = Options.validate(:cmd, \"echo\", [], cgroup_base: \"base\")\n    assert Map.has_key?(options, :cgroup_path)\n\n    [\"base\", other] = String.split(options.cgroup_path, \"/\")\n    assert byte_size(other) > 4\n  end\n\n  test \"disallow both cgroup_path and cgroup_base\" do\n    assert_raise ArgumentError, fn ->\n      Options.validate(:cmd, \"echo\", [], cgroup_base: \"base\", cgroup_path: \"path\")\n    end\n  end\n\n  test \"errors match System.cmd ones\" do\n    for context <- [:cmd, :daemon] do\n      # :enoent on missing executable\n      assert catch_error(Options.validate(context, \"__this_should_not_exist\", [], [])) == :enoent\n\n      assert_raise ArgumentError, fn ->\n        Options.validate(context, \"echo\", [~c\"not_a_binary\"], [])\n      end\n\n      assert_raise ArgumentError, fn ->\n        Options.validate(context, \"why\\0would_someone_do_this\", [], [])\n      end\n    end\n  end\n\n  test \"cmd and daemon-specific options\" do\n    # :cmd-only\n    assert Map.get(Options.validate(:cmd, \"echo\", [], into: \"\"), :into) == \"\"\n    assert Map.get(Options.validate(:cmd, \"echo\", [], timeout: 1000), :timeout) == 1000\n\n    assert_raise ArgumentError, fn ->\n      Options.validate(:daemon, \"echo\", [], into: \"\")\n    end\n\n    # :daemon-only\n    assert Map.get(Options.validate(:daemon, \"echo\", [], name: Something), :name) == Something\n\n    assert_raise ArgumentError, fn ->\n      Options.validate(:cmd, \"echo\", [], name: Something)\n    end\n\n    for level <- [:error, :warn, :info, :debug] do\n      assert Map.get(Options.validate(:daemon, \"echo\", [], log_output: level), :log_output) ==\n               level\n\n      assert_raise ArgumentError, fn ->\n        Options.validate(:cmd, \"echo\", [], log_output: level)\n      end\n    end\n\n    assert_raise ArgumentError, fn ->\n      Options.validate(:daemon, \"echo\", [], log_output: :bad_level)\n    end\n\n    assert_raise ArgumentError, fn ->\n      Options.validate(:daemon, \"echo\", [], timeout: 1000)\n    end\n\n    assert Map.get(\n             Options.validate(:daemon, \"echo\", [], logger_metadata: [foo: :bar]),\n             :logger_metadata\n           ) == [foo: :bar]\n\n    assert_raise ArgumentError, fn ->\n      Options.validate(:cmd, \"echo\", [], logger_metadata: [foo: :bar])\n    end\n\n    assert is_function(\n             Map.get(\n               Options.validate(:daemon, \"echo\", [], logger_fun: &Function.identity/1),\n               :logger_fun\n             )\n           )\n\n    assert {Function, :identity, []} =\n             Map.get(\n               Options.validate(:daemon, \"echo\", [], logger_fun: {Function, :identity, []}),\n               :logger_fun\n             )\n\n    assert {Function, :identity, []} =\n             Map.get(\n               Options.validate(:daemon, \"echo\", [], logger_fun: {Function, :identity}),\n               :logger_fun\n             )\n\n    assert_raise ArgumentError, fn ->\n      Options.validate(:daemon, \"echo\", [], logger_fun: &DateTime.add/2)\n    end\n\n    assert_raise ArgumentError, fn ->\n      Options.validate(:cmd, \"echo\", [], logger_fun: &Function.identity/1)\n    end\n  end\n\n  test \"common commands basically work\" do\n    input = [\n      cd: \"path\",\n      arg0: \"arg0\",\n      stderr_to_stdout: true,\n      capture_stderr_only: true,\n      parallelism: true,\n      uid: 5,\n      gid: \"bill\",\n      delay_to_sigkill: 1,\n      stdio_window: 1024,\n      env: [{\"KEY\", \"VALUE\"}, {\"KEY2\", \"VALUE2\"}],\n      cgroup_controllers: [\"memory\", \"cpu\"],\n      cgroup_base: \"base\",\n      cgroup_sets: [{\"memory\", \"memory.limit_in_bytes\", \"268435456\"}]\n    ]\n\n    for context <- [:daemon, :cmd] do\n      options = Options.validate(context, \"echo\", [], input)\n\n      assert Map.get(options, :cd) == \"path\"\n      assert Map.get(options, :arg0) == \"arg0\"\n      assert Map.get(options, :stderr_to_stdout) == true\n      assert Map.get(options, :capture_stderr_only) == true\n      assert Map.get(options, :parallelism) == true\n      assert Map.get(options, :uid) == 5\n      assert Map.get(options, :gid) == \"bill\"\n      assert Map.get(options, :delay_to_sigkill) == 1\n      assert Map.get(options, :stdio_window) == 1024\n      assert Map.get(options, :env) == [{~c\"KEY\", ~c\"VALUE\"}, {~c\"KEY2\", ~c\"VALUE2\"}]\n      assert Map.get(options, :cgroup_controllers) == [\"memory\", \"cpu\"]\n      assert Map.get(options, :cgroup_base) == \"base\"\n      assert Map.get(options, :cgroup_sets) == [{\"memory\", \"memory.limit_in_bytes\", \"268435456\"}]\n    end\n  end\nend\n"
  },
  {
    "path": "test/port_test.exs",
    "content": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2023 Jon Carstens\n#\n# SPDX-License-Identifier: Apache-2.0\n\ndefmodule MuonTrapPortTest do\n  use ExUnit.Case\n\n  test \"handles basic port call\" do\n    options = %{cmd: \"/bin/echo\", args: [\"1\", \"2\", \"3\"]}\n    port_options = MuonTrap.Port.port_options(options)\n\n    assert port_options == [\n             :use_stdio,\n             :exit_status,\n             :binary,\n             :hide,\n             {:args, [\"--\", \"/bin/echo\", \"1\", \"2\", \"3\"]}\n           ]\n  end\n\n  test \"handles cgroup controllers\" do\n    options = %{cmd: \"/bin/echo\", args: [], cgroup_controllers: [\"cpu\", \"memory\"]}\n    port_options = MuonTrap.Port.port_options(options)\n\n    assert Keyword.get(port_options, :args) == [\n             \"--controller\",\n             \"cpu\",\n             \"--controller\",\n             \"memory\",\n             \"--\",\n             \"/bin/echo\"\n           ]\n  end\n\n  test \"handles cgroup path\" do\n    options = %{cmd: \"/bin/echo\", args: [], cgroup_path: \"test/path\"}\n    port_options = MuonTrap.Port.port_options(options)\n\n    assert Keyword.get(port_options, :args) == [\n             \"--group\",\n             \"test/path\",\n             \"--\",\n             \"/bin/echo\"\n           ]\n  end\n\n  test \"handles cgroup sets\" do\n    options = %{cmd: \"/bin/echo\", args: [], cgroup_sets: [{\"cpu\", \"cpu.cfs_period_us\", \"100000\"}]}\n    port_options = MuonTrap.Port.port_options(options)\n\n    assert Keyword.get(port_options, :args) == [\n             \"--controller\",\n             \"cpu\",\n             \"--set\",\n             \"cpu.cfs_period_us=100000\",\n             \"--\",\n             \"/bin/echo\"\n           ]\n  end\n\n  test \"handles cgroup sets 2\" do\n    options = %{\n      cmd: \"/bin/echo\",\n      args: [],\n      cgroup_sets: [{\"cpu\", \"cpu.cfs_period_us\", \"100000\"}, {\"cpu\", \"cpu.cfs_quota_us\", \"50000\"}]\n    }\n\n    port_options = MuonTrap.Port.port_options(options)\n\n    assert Keyword.get(port_options, :args) == [\n             \"--controller\",\n             \"cpu\",\n             \"--set\",\n             \"cpu.cfs_period_us=100000\",\n             \"--controller\",\n             \"cpu\",\n             \"--set\",\n             \"cpu.cfs_quota_us=50000\",\n             \"--\",\n             \"/bin/echo\"\n           ]\n  end\n\n  test \"handles uid\" do\n    options = %{\n      cmd: \"/bin/echo\",\n      args: [],\n      uid: 1234\n    }\n\n    port_options = MuonTrap.Port.port_options(options)\n\n    assert Keyword.get(port_options, :args) == [\n             \"--uid\",\n             \"1234\",\n             \"--\",\n             \"/bin/echo\"\n           ]\n\n    options = %{\n      cmd: \"/bin/echo\",\n      args: [],\n      uid: \"bob\"\n    }\n\n    port_options = MuonTrap.Port.port_options(options)\n\n    assert Keyword.get(port_options, :args) == [\n             \"--uid\",\n             \"bob\",\n             \"--\",\n             \"/bin/echo\"\n           ]\n  end\n\n  test \"handles gid\" do\n    options = %{\n      cmd: \"/bin/echo\",\n      args: [],\n      gid: 14\n    }\n\n    port_options = MuonTrap.Port.port_options(options)\n\n    assert Keyword.get(port_options, :args) == [\n             \"--gid\",\n             \"14\",\n             \"--\",\n             \"/bin/echo\"\n           ]\n\n    options = %{\n      cmd: \"/bin/echo\",\n      args: [],\n      gid: \"bob\"\n    }\n\n    port_options = MuonTrap.Port.port_options(options)\n\n    assert Keyword.get(port_options, :args) == [\n             \"--gid\",\n             \"bob\",\n             \"--\",\n             \"/bin/echo\"\n           ]\n  end\n\n  test \"parses delay-to-sigkill\" do\n    options = %{\n      cmd: \"/bin/echo\",\n      args: [],\n      delay_to_sigkill: 123\n    }\n\n    port_options = MuonTrap.Port.port_options(options)\n\n    assert Keyword.get(port_options, :args) == [\n             \"--delay-to-sigkill\",\n             \"123\",\n             \"--\",\n             \"/bin/echo\"\n           ]\n  end\n\n  test \"parses stdio-window\" do\n    options = %{\n      cmd: \"/bin/echo\",\n      args: [],\n      stdio_window: 32\n    }\n\n    port_options = MuonTrap.Port.port_options(options)\n\n    assert Keyword.get(port_options, :args) == [\n             \"--stdio-window\",\n             \"32\",\n             \"--\",\n             \"/bin/echo\"\n           ]\n  end\n\n  defp encode_acks(number) do\n    number\n    |> MuonTrap.Port.encode_acks()\n    |> IO.iodata_to_binary()\n  end\n\n  test \"ack calculation\" do\n    assert encode_acks(1) == <<0>>\n    assert encode_acks(10) == <<9>>\n    assert encode_acks(256) == <<255>>\n    assert encode_acks(257) == <<255, 0>>\n    assert encode_acks(512) == <<255, 255>>\n    assert encode_acks(513) == <<255, 255, 0>>\n  end\nend\n"
  },
  {
    "path": "test/print_a_lot.c",
    "content": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n// SPDX-FileCopyrightText: 2023 Jon Carstens\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <stdio.h>\n#include <stdlib.h>\n#include <unistd.h>\n\nint main()\n{\n    int i;\n    for (i = 0; i < 1000; i++) {\n        printf(\"%d-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\n\", i);\n    }\n    fflush(stdout);\n\n    // Sleep a little since muontrap doesn't wait for all output to be consumed\n    sleep(1);\n    exit(0);\n}\n"
  },
  {
    "path": "test/succeed_second_time.c",
    "content": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <err.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <unistd.h>\n\nstatic int read_counter(const char *filename)\n{\n    FILE *fp = fopen(filename, \"r\");\n    if (!fp)\n        return 0;\n\n    int counter;\n    if (fscanf(fp, \"%d\", &counter) != 1)\n        counter = 0;\n    fclose(fp);\n    return counter;\n}\n\nstatic void write_counter(const char *filename, int counter)\n{\n    FILE *fp = fopen(filename, \"w\");\n    fprintf(fp, \"%d\\n\", counter);\n    fclose(fp);\n}\n\nint main(int argc, char **argv)\n{\n    if (argc != 2)\n        errx(EXIT_FAILURE, \"Pass a filename\");\n\n    int counter = read_counter(argv[1]);\n    printf(\"Called %d times\\n\", counter);\n    write_counter(argv[1], counter + 1);\n\n    // Only exit successful on the second call.\n    if (counter == 1)\n        exit(EXIT_SUCCESS);\n    else\n        exit(EXIT_FAILURE);\n}\n"
  },
  {
    "path": "test/support/test_case.ex",
    "content": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n#\n# SPDX-License-Identifier: Apache-2.0\n\ndefmodule MuonTrapTest.Case do\n  @moduledoc false\n  use ExUnit.CaseTemplate\n\n  using do\n    quote do\n      import unquote(__MODULE__)\n      alias MuonTrapTest.Case\n    end\n  end\n\n  @timeout_before_close_check 20\n\n  @spec test_path(Path.t()) :: Path.t()\n  def test_path(cmd) do\n    Path.join([File.cwd!(), \"test\", cmd])\n  end\n\n  @spec cpu_cgroup_exists(String.t()) :: boolean\n  def cpu_cgroup_exists(path) do\n    {rc, 0} = System.cmd(\"cgget\", [\"-g\", \"cpu\", path], stderr_to_stdout: true)\n    String.match?(rc, ~r/cpu.shares/)\n  end\n\n  @spec memory_cgroup_exists(String.t()) :: boolean\n  def memory_cgroup_exists(path) do\n    {rc, 0} = System.cmd(\"cgget\", [\"-g\", \"memory\", path], stderr_to_stdout: true)\n    String.match?(rc, ~r/memory.stat/)\n  end\n\n  @spec random_cgroup_path :: String.t()\n  def random_cgroup_path() do\n    \"muontrap_test/test#{:rand.uniform(10000)}\"\n  end\n\n  @spec os_pid_around?(non_neg_integer()) :: boolean\n  def os_pid_around?(os_pid) do\n    {_, rc} = System.cmd(\"ps\", [\"-p\", \"#{os_pid}\"])\n    rc == 0\n  end\n\n  @spec assert_os_pid_running(non_neg_integer()) :: :ok\n  def assert_os_pid_running(os_pid) do\n    os_pid_around?(os_pid) || flunk(\"Expected OS pid #{os_pid} to still be running\")\n    :ok\n  end\n\n  @spec assert_os_pid_exited(non_neg_integer()) :: :ok\n  def assert_os_pid_exited(os_pid) do\n    os_pid_around?(os_pid) && flunk(\"Expected OS pid #{os_pid} to be killed\")\n    :ok\n  end\n\n  @spec os_pid(port()) :: non_neg_integer()\n  def os_pid(port) do\n    {:os_pid, os_pid} = Port.info(port, :os_pid)\n    os_pid\n  end\n\n  @spec wait_for_close_check(non_neg_integer()) :: :ok\n  def wait_for_close_check(timeout \\\\ @timeout_before_close_check) do\n    Process.sleep(timeout)\n  end\nend\n"
  },
  {
    "path": "test/test_helper.exs",
    "content": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2023 Jon Carstens\n#\n# SPDX-License-Identifier: Apache-2.0\n\nExUnit.start()\n\ndefmodule MuonTrapTestHelpers do\n  @spec check_cgroup_support() :: :ok | no_return()\n  def check_cgroup_support() do\n    if !System.find_executable(\"cgget\") do\n      IO.puts(:stderr, \"\\nPlease install cgroup-tools so that cgcreate and cgget are available.\")\n      IO.puts(:stderr, \"\\nTo skip cgroup tests, run `mix test --exclude cgroup`\")\n      System.halt(1)\n    end\n\n    if !(MuonTrapTest.Case.cpu_cgroup_exists(\"muontrap_test\") and\n           MuonTrapTest.Case.memory_cgroup_exists(\"muontrap_test\")) do\n      IO.puts(:stderr, \"\\nPlease create the muontrap_test cgroup\")\n      IO.puts(:stderr, \"sudo cgcreate -a $(whoami) -g memory,cpu:muontrap_test\")\n      IO.puts(:stderr, \"\\nTo skip cgroup tests, run `mix test --exclude cgroup`\")\n      System.halt(1)\n    end\n  end\n\n  @spec cgroup_excluded?() :: boolean\n  def cgroup_excluded?() do\n    excludes = ExUnit.configuration()[:exclude]\n\n    :cgroup in excludes or truthy?(Keyword.get(excludes, :cgroup))\n  end\n\n  defp truthy?(\"false\"), do: false\n  defp truthy?(false), do: false\n  defp truthy?(nil), do: false\n  defp truthy?(_), do: true\nend\n\nif !MuonTrapTestHelpers.cgroup_excluded?() do\n  case :os.type() do\n    {:unix, :linux} ->\n      MuonTrapTestHelpers.check_cgroup_support()\n\n    _ ->\n      IO.puts(:stderr, \"Not on Linux so skipping tests that use cgroups...\")\n      ExUnit.configure(exclude: :cgroup)\n  end\nend\n"
  }
]