diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..16f4324 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,39 @@ +version: "2" + +checks: + argument-count: + enabled: true + config: + threshold: 6 + complex-logic: + enabled: true + config: + threshold: 6 + file-lines: + enabled: true + config: + threshold: 1000 + method-complexity: + enabled: true + config: + threshold: 8 + method-count: + enabled: true + config: + threshold: 20 + method-lines: + enabled: true + config: + threshold: 100 + nested-control-flow: + enabled: true + config: + threshold: 6 + return-statements: + enabled: true + config: + threshold: 6 + similar-code: + enabled: false + identical-code: + enabled: false diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..45abf3d --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,42 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of +fostering an open and welcoming community, we pledge to respect all people who +contribute through reporting issues, posting feature requests, updating +documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free +experience for everyone, regardless of level of experience, gender, gender +identity and expression, sexual orientation, disability, personal appearance, +body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic + addresses, without explicit permission +* Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to +fairly and consistently applying these principles to every aspect of managing +this project. Project maintainers who do not follow or enforce the Code of +Conduct may be permanently removed from the project team. + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting a project maintainer at `conduct@essentialkaos.com`. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. Maintainers are +obligated to maintain confidentiality with regard to the reporter of an +incident. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..d7da3fb --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Contributing Guidelines + +**IMPORTANT! Contribute your code only if you have an excellent understanding of project idea and all existing code base. Otherwise, a nicely formatted issue will be more helpful to us.** + +### Issues + +1. Provide product version where the problem was found; +2. Provide info about your environment; +3. Provide detailed info about your problem; +4. Provide steps to reproduce the problem; +5. Provide actual and expected results. + +### Code + +1. Check your code **before** creating pull request; +2. If tests are present in a project, add tests for your code; +3. Add inline documentation for your code; +4. Apply code style used throughout the project; +5. Create your pull request to `develop` branch (_pull requests to other branches are not allowed_). diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..470e5a6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,64 @@ +name: ❗ Bug Report +description: File a bug report +title: "[Bug]: " +labels: ["issue • bug"] +assignees: + - andyone + +body: + - type: markdown + attributes: + value: | + > [!IMPORTANT] + > Before you open an issue, search GitHub Issues for a similar bug reports. If so, please add a 👍 reaction to the existing issue. + + - type: textarea + attributes: + label: Verbose application info + description: Output of `swap-reaper -vv` command + render: shell + validations: + required: true + + - type: dropdown + id: version + attributes: + label: Install tools + description: How did you install this application + options: + - From Sources + - RPM Package + - Prebuilt Binary + default: 0 + validations: + required: true + + - type: textarea + attributes: + label: Steps to reproduce + description: Short guide on how to reproduce this problem on our site + placeholder: | + 1. [First Step] + 2. [Second Step] + 3. [and so on...] + validations: + required: true + + - type: textarea + attributes: + label: Expected behavior + description: What you expected to happen + validations: + required: true + + - type: textarea + attributes: + label: Actual behavior + description: What actually happened + validations: + required: true + + - type: textarea + attributes: + label: Additional info + description: Include gist of relevant config, logs, etc. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000..6be6a96 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,26 @@ +name: ❓ Question +description: Question about application, configuration or code +title: "[Question]: " +labels: ["issue • question"] +assignees: + - andyone + +body: + - type: markdown + attributes: + value: | + > [!IMPORTANT] + > Before you open an issue, search GitHub Issues for a similar question. If so, please add a 👍 reaction to the existing issue. + + - type: textarea + attributes: + label: Question + description: Detailed question + validations: + required: true + + - type: textarea + attributes: + label: Related version application info + description: Output of `swap-reaper -vv` command + render: shell diff --git a/.github/ISSUE_TEMPLATE/suggestion.yml b/.github/ISSUE_TEMPLATE/suggestion.yml new file mode 100644 index 0000000..39c69e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/suggestion.yml @@ -0,0 +1,43 @@ +name: ➕ Suggestion +description: Suggest new feature or improvement +title: "[Suggestion]: " +labels: ["issue • suggestion"] +assignees: + - andyone + +body: + - type: markdown + attributes: + value: | + > [!IMPORTANT] + > Before you open an issue, search GitHub Issues for a similar feature requests. If so, please add a 👍 reaction to the existing issue. + > + > Opening a feature request kicks off a discussion. Requests may be closed if we're not actively planning to work on them. + + - type: textarea + attributes: + label: Proposal + description: Description of the feature + validations: + required: true + + - type: textarea + attributes: + label: Current behavior + description: What currently happens + validations: + required: true + + - type: textarea + attributes: + label: Desired behavior + description: What you would like to happen + validations: + required: true + + - type: textarea + attributes: + label: Use case + description: Why is this important (helps with prioritizing requests) + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e7814b8 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,24 @@ +### What did you implement: + +Closes #XXXXX + +### How did you implement it: + +... + +### How can we verify it: + +... + +### TODO's: + +- [ ] Write tests +- [ ] Write documentation +- [ ] Check that there aren't other open pull requests for the same issue/feature +- [ ] Format your source code by `make fmt` +- [ ] Provide verification config / commands +- [ ] Enable "Allow edits from maintainers" for this PR +- [ ] Update the messages below + +**Is this ready for review?:** No +**Is it a breaking change?:** No diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..605744a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,36 @@ +version: 2 + +updates: + - package-ecosystem: "gomod" + directory: "/" + target-branch: "develop" + schedule: + interval: "daily" + timezone: "Etc/UTC" + time: "03:00" + labels: + - "PR • MAINTENANCE" + assignees: + - "andyone" + reviewers: + - "andyone" + groups: + all: + applies-to: version-updates + update-types: + - "minor" + - "patch" + + - package-ecosystem: "github-actions" + directory: "/" + target-branch: "develop" + schedule: + interval: "daily" + timezone: "Etc/UTC" + time: "03:00" + labels: + - "PR • MAINTENANCE" + assignees: + - "andyone" + reviewers: + - "andyone" diff --git a/.github/images/card.svg b/.github/images/card.svg new file mode 100644 index 0000000..a743507 --- /dev/null +++ b/.github/images/card.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.github/images/license.svg b/.github/images/license.svg new file mode 100644 index 0000000..8e82228 --- /dev/null +++ b/.github/images/license.svg @@ -0,0 +1 @@ +license: Apache-2.0licenseApache-2.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a9d689e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,105 @@ +name: CI + +on: + push: + branches: [master, develop] + pull_request: + branches: [master] + workflow_dispatch: + inputs: + force_run: + description: 'Force workflow run' + required: true + type: choice + options: [yes, no] + +permissions: + actions: read + contents: read + statuses: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + Go: + name: Go + runs-on: ubuntu-latest + + strategy: + matrix: + go: [ '1.21.x', '1.22.x' ] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + + - name: Download dependencies + run: make deps + + - name: Run tests + run: make all + + Aligo: + name: Aligo + runs-on: ubuntu-latest + + needs: Go + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.21.x' + + - name: Download dependencies + run: make deps + + - name: Check Golang sources with Aligo + uses: essentialkaos/aligo-action@v2 + with: + files: ./... + + Perfecto: + name: Perfecto + runs-on: ubuntu-latest + + needs: Go + + steps: + - name: Code checkout + uses: actions/checkout@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Check specs with Perfecto + uses: essentialkaos/perfecto-action@v2 + with: + files: common/swap-reaper.spec + + Typos: + name: Typos + runs-on: ubuntu-latest + + needs: Go + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check spelling + uses: crate-ci/typos@master diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..bfc4df5 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,33 @@ +name: "CodeQL" + +on: + push: + branches: [master, develop] + pull_request: + branches: [master] + schedule: + - cron: '0 3 * * */2' + +permissions: + security-events: write + actions: read + contents: read + +jobs: + analyse: + name: Analyse + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: go + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fda2703 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/swap-reaper +/vendor diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..375e6b3 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,2 @@ +[files] +extend-exclude = [ "go.sum", "vendor" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..12fb719 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 ESSENTIAL KAOS + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..53ebe91 --- /dev/null +++ b/Makefile @@ -0,0 +1,125 @@ +################################################################################ + +# This Makefile generated by GoMakeGen 3.2.0 using next command: +# gomakegen --mod . +# +# More info: https://kaos.sh/gomakegen + +################################################################################ + +ifdef VERBOSE ## Print verbose information (Flag) +VERBOSE_FLAG = -v +endif + +ifdef PROXY ## Force proxy usage for downloading dependencies (Flag) +export GOPROXY=https://proxy.golang.org/cached-only,direct +endif + +ifdef CGO ## Enable CGO usage (Flag) +export CGO_ENABLED=1 +else +export CGO_ENABLED=0 +endif + +COMPAT ?= 1.19 +MAKEDIR = $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) +GITREV ?= $(shell test -s $(MAKEDIR)/.git && git rev-parse --short HEAD) + +################################################################################ + +.DEFAULT_GOAL := help +.PHONY = fmt vet all install uninstall clean deps update init vendor mod-init mod-update mod-download mod-vendor help + +################################################################################ + +all: swap-reaper ## Build all binaries + +swap-reaper: + @echo "Building swap-reaper…" + @go build $(VERBOSE_FLAG) -ldflags="-X main.gitrev=$(GITREV)" swap-reaper.go + +install: ## Install all binaries + @echo "Installing binaries…" + @cp swap-reaper /usr/bin/swap-reaper + +uninstall: ## Uninstall all binaries + @echo "Removing installed binaries…" + @rm -f /usr/bin/swap-reaper + +init: mod-init ## Initialize new module + +deps: mod-download ## Download dependencies + +update: mod-update ## Update dependencies to the latest versions + +vendor: mod-vendor ## Make vendored copy of dependencies + +mod-init: + @echo "[1/2] Modules initialization…" +ifdef MODULE_PATH ## Module path for initialization (String) + @go mod init $(MODULE_PATH) +else + @go mod init +endif + + @echo "[2/2] Dependencies cleanup…" +ifdef COMPAT ## Compatible Go version (String) + @go mod tidy $(VERBOSE_FLAG) -compat=$(COMPAT) -go=$(COMPAT) +else + @go mod tidy $(VERBOSE_FLAG) +endif + +mod-update: + @echo "[1/4] Updating dependencies…" +ifdef UPDATE_ALL ## Update all dependencies (Flag) + @go get -u $(VERBOSE_FLAG) all +else + @go get -u $(VERBOSE_FLAG) ./... +endif + + @echo "[2/4] Stripping toolchain info…" + @grep -q 'toolchain ' go.mod && go mod edit -toolchain=none || : + + @echo "[3/4] Dependencies cleanup…" +ifdef COMPAT + @go mod tidy $(VERBOSE_FLAG) -compat=$(COMPAT) +else + @go mod tidy $(VERBOSE_FLAG) +endif + + @echo "[4/4] Updating vendored dependencies…" + @test -d vendor && rm -rf vendor && go mod vendor $(VERBOSE_FLAG) || : + +mod-download: + @echo "Downloading dependencies…" + @go mod download + +mod-vendor: + @echo "Vendoring dependencies…" + @rm -rf vendor && go mod vendor $(VERBOSE_FLAG) || : + +fmt: ## Format source code with gofmt + @echo "Formatting sources…" + @find . -name "*.go" -exec gofmt -s -w {} \; + +vet: ## Runs 'go vet' over sources + @echo "Running 'go vet' over sources…" + @go vet -composites=false -printfuncs=LPrintf,TLPrintf,TPrintf,log.Debug,log.Info,log.Warn,log.Error,log.Critical,log.Print ./... + +clean: ## Remove generated files + @echo "Removing built binaries…" + @rm -f swap-reaper + +help: ## Show this info + @echo -e '\n\033[1mTargets:\033[0m\n' + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ + | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[33m%-9s\033[0m %s\n", $$1, $$2}' + @echo -e '\n\033[1mVariables:\033[0m\n' + @grep -E '^ifdef [A-Z_]+ .*?## .*$$' $(abspath $(lastword $(MAKEFILE_LIST))) \ + | sed 's/ifdef //' \ + | sort -h \ + | awk 'BEGIN {FS = " .*?## "}; {printf " \033[32m%-11s\033[0m %s\n", $$1, $$2}' + @echo -e '' + @echo -e '\033[90mGenerated by GoMakeGen 3.2.0\033[0m\n' + +################################################################################ diff --git a/README.md b/README.md index d962f67..1e81edd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,40 @@ -# swap-reaper -Service to periodically clean swap memory +

+ +

+ Code Climate Maintainability + GitHub Actions CI Status + GitHub Actions CodeQL Status + +

+ +

InstallationCI StatusContributingLicense

+ +
+ +`swap-reaper` is a service to periodically clean swap memory. + +### Installation + +#### From [ESSENTIAL KAOS Public Repository](https://kaos.sh/kaos-repo) + +```bash +sudo dnf install -y https://pkgs.kaos.st/kaos-repo-latest.el$(grep 'CPE_NAME' /etc/os-release | tr -d '"' | cut -d':' -f5).noarch.rpm +sudo dnf install swap-reaper +``` + +### CI Status + +| Branch | Status | +|--------|----------| +| `master` | [![CI](https://kaos.sh/w/swap-reaper/ci.svg?branch=master)](https://kaos.sh/w/swap-reaper/ci?query=branch:master) | +| `develop` | [![CI](https://kaos.sh/w/swap-reaper/ci.svg?branch=develop)](https://kaos.sh/w/swap-reaper/ci?query=branch:develop) | + +### Contributing + +Before contributing to this project please read our [Contributing Guidelines](https://github.com/essentialkaos/contributing-guidelines#contributing-guidelines). + +### License + +[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) + +

diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..f42c71e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,36 @@ +# Security Policies and Procedures + +This document outlines security procedures and general policies for all +ESSENTIAL KAOS projects. + + * [Reporting a Bug](#reporting-a-bug) + * [Disclosure Policy](#disclosure-policy) + +## Reporting a Bug + +The ESSENTIAL KAOS team and community take all security bugs in our projects +very seriously. Thank you for improving the security of our project. We +appreciate your efforts and responsible disclosure and will make every effort +to acknowledge your contributions. + +Report security bugs by emailing our security team at security@essentialkaos.com. + +The security team will acknowledge your email within 48 hours and will send a +more detailed response within 48 hours, indicating the next steps in handling +your report. After the initial reply to your report, the security team will +endeavor to keep you informed of the progress towards a fix and full +announcement, and may ask for additional information or guidance. + +Report security bugs in third-party dependencies to the person or team +maintaining the dependencies. + +## Disclosure Policy + +When the security team receives a security bug report, they will assign it to a +primary handler. This person will coordinate the fix and release process, +involving the following steps: + + * Confirm the problem and determine the affected versions; + * Audit code to find any similar potential problems; + * Prepare fixes for all releases still under maintenance. These fixes will be + released as fast as possible. diff --git a/common/swap-reaper.knf b/common/swap-reaper.knf new file mode 100644 index 0000000..411737c --- /dev/null +++ b/common/swap-reaper.knf @@ -0,0 +1,23 @@ +# This is configuration file for swap-reaper + +[limits] + + # If 1 min LA is greater than given value cleaning will be postponed (> 0.1) + max-la: 1.0 + + # Max time in minutes to wait for LA to normalize (1-86,400) + max-wait: 15 + +[log] + + # Log file dir + dir: /var/log/swap-reaper + + # Path to log file + file: {log:dir}/swap-reaper.log + + # Log permissions + perms: 644 + + # Default log level (debug/info/warn/error/crit) + level: info diff --git a/common/swap-reaper.logrotate b/common/swap-reaper.logrotate new file mode 100644 index 0000000..b8abfc0 --- /dev/null +++ b/common/swap-reaper.logrotate @@ -0,0 +1,9 @@ +/var/log/swap-reaper/*.log { + weekly + rotate 8 + copytruncate + delaycompress + compress + notifempty + missingok +} diff --git a/common/swap-reaper.service b/common/swap-reaper.service new file mode 100644 index 0000000..7d95b52 --- /dev/null +++ b/common/swap-reaper.service @@ -0,0 +1,16 @@ +[Unit] +Description=Tool to periodically clean swap memory +Documentation=https://kaos.sh/swap-reaper +After=network-online.target remote-fs.target nss-lookup.target +Wants=network-online.target + +[Service] +Type=simple +ExecStart=/usr/bin/swap-reaper -c /etc/swap-reaper.knf +ExecReload=/bin/kill -s HUP $MAINPID +ExecStop=/bin/kill -s TERM $MAINPID +StandardError=file:/var/log/swap-reaper/startup.log +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/common/swap-reaper.spec b/common/swap-reaper.spec new file mode 100644 index 0000000..2a62af1 --- /dev/null +++ b/common/swap-reaper.spec @@ -0,0 +1,109 @@ +################################################################################ + +%global crc_check pushd ../SOURCES ; sha512sum -c %{SOURCE100} ; popd + +################################################################################ + +%define debug_package %{nil} + +################################################################################ + +%define _logdir %{_localstatedir}/log + +################################################################################ + +Summary: Tool to periodically clean swap memory +Name: swap-reaper +Version: 0.0.1 +Release: 0%{?dist} +Group: Applications/System +License: Apache License, Version 2.0 +URL: https://kaos.sh/swap-reaper + +Source0: https://source.kaos.st/%{name}/%{name}-%{version}.tar.bz2 + +Source100: checksum.sha512 + +BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) + +BuildRequires: golang >= 1.22 + +Requires: systemd + +Provides: %{name} = %{version}-%{release} + +################################################################################ + +%description +Tool to periodically clean swap memory. + +################################################################################ + +%prep +%{crc_check} + +%setup -q +if [[ ! -d "%{name}/vendor" ]] ; then + echo -e "----\nThis package requires vendored dependencies\n----" + exit 1 +elif [[ -f "%{name}/%{name}" ]] ; then + echo -e "----\nSources must not contain precompiled binaries\n----" + exit 1 +fi + +%build +pushd %{name} + %{__make} %{?_smp_mflags} all + cp LICENSE .. +popd + +%install +rm -rf %{buildroot} + +install -dDm 755 %{buildroot}%{_bindir} +install -dDm 755 %{buildroot}%{_sysconfdir}/logrotate.d +install -dDm 755 %{buildroot}%{_unitdir} +install -dDm 755 %{buildroot}%{_logdir}/%{name} + +install -pm 755 %{name}/%{name} \ + %{buildroot}%{_bindir}/ + +install -pm 644 %{name}/common/%{name}.knf \ + %{buildroot}%{_sysconfdir}/ + +install -pm 644 %{name}/common/%{name}.logrotate \ + %{buildroot}%{_sysconfdir}/logrotate.d/%{name} + +install -pm 644 %{name}/common/%{name}.service \ + %{buildroot}%{_unitdir}/ + +%clean +rm -rf %{buildroot} + +%preun +if [[ $1 -eq 0 ]] ; then + systemctl --no-reload disable %{name}.service &>/dev/null || : + systemctl stop %{name}.service &>/dev/null || : +fi + +%postun +if [[ $1 -ge 1 ]] ; then + systemctl daemon-reload &>/dev/null || : +fi + +################################################################################ + +%files +%defattr(-,root,root,-) +%doc LICENSE +%dir %{_logdir}/%{name} +%config(noreplace) %{_sysconfdir}/%{name}.knf +%config(noreplace) %{_sysconfdir}/logrotate.d/%{name} +%{_unitdir}/%{name}.service +%{_bindir}/%{name} + +################################################################################ + +%changelog +* Thu Sep 12 2024 Anton Novojilov - 0.0.1-0 +- The very first version diff --git a/daemon/daemon.go b/daemon/daemon.go new file mode 100644 index 0000000..1efd4d5 --- /dev/null +++ b/daemon/daemon.go @@ -0,0 +1,456 @@ +package daemon + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2024 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "fmt" + "os" + "os/exec" + "strings" + "time" + + "github.com/essentialkaos/ek/v13/errutil" + "github.com/essentialkaos/ek/v13/fmtc" + "github.com/essentialkaos/ek/v13/fmtutil" + "github.com/essentialkaos/ek/v13/knf" + "github.com/essentialkaos/ek/v13/log" + "github.com/essentialkaos/ek/v13/mathutil" + "github.com/essentialkaos/ek/v13/options" + "github.com/essentialkaos/ek/v13/signal" + "github.com/essentialkaos/ek/v13/support" + "github.com/essentialkaos/ek/v13/support/deps" + "github.com/essentialkaos/ek/v13/support/kernel" + "github.com/essentialkaos/ek/v13/support/resources" + "github.com/essentialkaos/ek/v13/system" + "github.com/essentialkaos/ek/v13/system/sysctl" + "github.com/essentialkaos/ek/v13/terminal" + "github.com/essentialkaos/ek/v13/terminal/tty" + "github.com/essentialkaos/ek/v13/timeutil" + "github.com/essentialkaos/ek/v13/usage" + + knfv "github.com/essentialkaos/ek/v13/knf/validators" + knff "github.com/essentialkaos/ek/v13/knf/validators/fs" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Basic service info +const ( + APP = "swap-reaper" + VER = "0.0.1" + DESC = "Service to periodically clean swap memory" +) + +// Options +const ( + OPT_CONFIG = "c:config" + OPT_NO_COLOR = "nc:no-color" + OPT_HELP = "h:help" + OPT_VER = "v:version" + + OPT_VERB_VER = "vv:verbose-version" +) + +// Configuration file properties +const ( + LIMITS_MAX_LA = "limits:max-la" + LIMITS_MAX_WAIT = "limits:max-wait" + LOG_DIR = "log:dir" + LOG_FILE = "log:file" + LOG_PERMS = "log:perms" + LOG_LEVEL = "log:level" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// optMap contains information about all supported options +var optMap = options.Map{ + OPT_CONFIG: {Value: "/etc/swap-reaper.knf"}, + OPT_NO_COLOR: {Type: options.BOOL}, + OPT_HELP: {Type: options.BOOL}, + OPT_VER: {Type: options.MIXED}, + + OPT_VERB_VER: {Type: options.BOOL}, +} + +// color tags for app name and version +var colorTagApp, colorTagVer string + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Run is main daemon function +func Run(gitRev string, gomod []byte) { + preConfigureUI() + + _, errs := options.Parse(optMap) + + if !errs.IsEmpty() { + terminal.Error("Options parsing errors:") + terminal.Error(errs.String()) + os.Exit(1) + } + + configureUI() + + switch { + case options.GetB(OPT_VER): + genAbout(gitRev).Print(options.GetS(OPT_VER)) + os.Exit(0) + case options.GetB(OPT_HELP): + genUsage().Print() + os.Exit(0) + case options.GetB(OPT_VERB_VER): + support.Collect(APP, VER). + WithRevision(gitRev). + WithDeps(deps.Extract(gomod)). + WithResources(resources.Collect()). + WithKernel(kernel.Collect("vm.swappiness")). + Print() + os.Exit(0) + } + + err := errutil.Chain( + checkUser, + loadConfig, + validateConfig, + registerSignalHandlers, + setupLogger, + ) + + if err != nil { + printErrorAndExit(err.Error()) + } + + log.Aux(strings.Repeat("-", 80)) + log.Aux("%s %s starting…", APP, VER) + + err = start() + + if err != nil { + log.Crit(err.Error()) + os.Exit(1) + } +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// preConfigureUI preconfigures UI based on information about user terminal +func preConfigureUI() { + if !tty.IsTTY() || tty.IsSystemd() { + fmtc.DisableColors = true + } + + switch { + case fmtc.IsTrueColorSupported(): + colorTagApp, colorTagVer = "{*}{#00AFFF}", "{#00AFFF}" + case fmtc.Is256ColorsSupported(): + colorTagApp, colorTagVer = "{*}{#39}", "{#39}" + default: + colorTagApp, colorTagVer = "{*}{c}", "{c}" + } +} + +// configureUI configures user interface +func configureUI() { + if options.GetB(OPT_NO_COLOR) { + fmtc.DisableColors = true + } +} + +// checkUser checks if current user is root +func checkUser() error { + user, err := system.CurrentUser() + + if err != nil { + return fmt.Errorf("Can't get info about current user: %v", err) + } + + if !user.IsRoot() { + return fmt.Errorf("You must run this daemon as super user (root)") + } + + return nil +} + +// loadConfig loads configuration file +func loadConfig() error { + err := knf.Global(options.GetS(OPT_CONFIG)) + + if err != nil { + return fmt.Errorf("Can't load configuration: %w", err) + } + + return nil +} + +// validateConfig validates configuration file values +func validateConfig() error { + errs := knf.Validate([]*knf.Validator{ + {LIMITS_MAX_LA, knfv.TypeFloat, nil}, + {LIMITS_MAX_LA, knfv.Greater, 0.1}, + {LIMITS_MAX_WAIT, knfv.Greater, 1}, + {LIMITS_MAX_WAIT, knfv.Less, 24 * 3600}, + {LOG_DIR, knff.Perms, "DWX"}, + {LOG_LEVEL, knfv.SetToAnyIgnoreCase, []string{ + "debug", "info", "warn", "error", "crit", + }}, + }) + + if len(errs) != 0 { + return fmt.Errorf("Configuration file validation error: %w", errs[0]) + } + + return nil +} + +// registerSignalHandlers registers signal handlers +func registerSignalHandlers() error { + signal.Handlers{ + signal.TERM: termSignalHandler, + signal.INT: intSignalHandler, + signal.HUP: hupSignalHandler, + }.TrackAsync() + + return nil +} + +// setupLogger configures logger subsystem +func setupLogger() error { + err := log.Set(knf.GetS(LOG_FILE), knf.GetM(LOG_PERMS, 644)) + + if err != nil { + return err + } + + err = log.MinLevel(knf.GetS(LOG_LEVEL)) + + if err != nil { + return err + } + + return nil +} + +// start starts daemon +func start() error { + mem, err := system.GetMemUsage() + + if err != nil { + return fmt.Errorf("Can't get memory usage info: %v", err) + } + + if mem.SwapTotal == 0 { + return fmt.Errorf("Swap is disabled, nothing to do…") + } + + swappiness, err := sysctl.GetI("vm.swappiness") + + if err != nil { + return fmt.Errorf("Can't read swappiness configuration: %v", err) + } + + if swappiness > 30 { + log.Warn("The kernel parameter 'vm.swappiness' is too high! A value between 30 and 5 is recommended.") + } + + log.Aux("Initialization finished, monitoring system swap…") + + checkLoop(swappiness) + + return nil +} + +// checkLoop is function with check loop +func checkLoop(swappiness int) { + var err error + var mem *system.MemUsage + var maxMem float64 + var lastCheck time.Time + + maxWait := time.Duration(knf.GetI(LIMITS_MAX_WAIT, 10)) * time.Minute + maxLA := knf.GetF(LIMITS_MAX_LA, 0.1) + + for range time.NewTicker(time.Minute).C { + mem, err = system.GetMemUsage() + + if err != nil { + log.Error("Can't get system memory usage: %v", err) + continue + } + + if maxMem == 0 { + maxMem = float64(mem.MemTotal) - mathutil.FromPerc(float64(swappiness), float64(mem.MemTotal)) + } + + log.Debug( + "Memory: %s / %s | Swap: %s / %s | Swappiness: %s (≥ %s)", + fmtutil.PrettySize(mem.MemUsed), fmtutil.PrettySize(mem.MemTotal), + fmtutil.PrettySize(mem.SwapUsed), fmtutil.PrettySize(mem.SwapTotal), + fmtutil.PrettyPerc(float64(swappiness)), fmtutil.PrettySize(maxMem), + ) + + if mem.SwapUsed == 0 { + continue + } + + if mem.SwapUsed+mem.MemUsed > uint64(maxMem) { + log.Warn( + "Not enough memory to clean up swap: swap (%s) + used (%s) > %s (swappiness %s)", + fmtutil.PrettySize(mem.MemUsed), fmtutil.PrettySize(mem.SwapUsed), + fmtutil.PrettySize(maxMem), fmtutil.PrettyPerc(float64(swappiness)), + ) + continue + } + + la, err := system.GetLA() + + if err != nil { + log.Error("Can't check system LA: %v", err) + continue + } + + if la.Min1 >= maxLA && maxWait != time.Minute { + if lastCheck.IsZero() { + lastCheck = time.Now() + log.Warn( + "System LA is too big (%s ≥ %s), cleaning is delayed (max wait: %d min)", + fmtutil.PrettyNum(la.Min1), fmtutil.PrettyNum(maxLA), + knf.GetI(LIMITS_MAX_WAIT, 10), + ) + continue + } else { + if time.Since(lastCheck) < maxWait { + continue + } + + log.Warn( + "System LA is too big (%s ≥ %s), but we've reached the maximum wait limit (%d min). Clean anyway…", + fmtutil.PrettyNum(la.Min1), fmtutil.PrettyNum(maxLA), + knf.GetI(LIMITS_MAX_WAIT, 10), + ) + } + } + + log.Info("Found swap to clean (%s), cleaning…", fmtutil.PrettySize(mem.SwapUsed)) + + start := time.Now() + err = cleanSwap() + + if err != nil { + log.Error(err.Error()) + continue + } + + newMem, err := system.GetMemUsage() + + if err != nil { + log.Info( + "Data successfully moved from swap to memory (took %s)", + timeutil.ShortDuration(time.Since(start), true), + ) + } else { + log.Info( + "Data successfully moved from swap to memory (took %s). Memory: %s / %s (%s)", + timeutil.ShortDuration(time.Since(start), true), + fmtutil.PrettySize(newMem.MemUsed), fmtutil.PrettySize(newMem.MemTotal), + fmtutil.PrettyPerc(mathutil.Perc(newMem.MemUsed, newMem.MemTotal)), + ) + } + + lastCheck = time.Time{} + } +} + +// cleanSwap moves data from swap to memory using swapoff & swapon +func cleanSwap() error { + cmdOff := exec.Command("swapoff", "-a") + err := cmdOff.Run() + + if err != nil { + ec := cmdOff.ProcessState.ExitCode() + return fmt.Errorf("Can't disable swap using swapoff: swapoff exited with code %d", ec) + } + + cmdOn := exec.Command("swapon", "-a") + err = cmdOn.Run() + + if err != nil { + ec := cmdOff.ProcessState.ExitCode() + return fmt.Errorf("Can't enable swap back using swapon: swapon exited with code %d", ec) + } + + return nil +} + +// intSignalHandler is INT signal handler +func intSignalHandler() { + log.Aux("Received INT signal, shutdown…") + shutdown(0) +} + +// termSignalHandler is TERM signal handler +func termSignalHandler() { + log.Aux("Received TERM signal, shutdown…") + shutdown(0) +} + +// hupSignalHandler is HUP signal handler +func hupSignalHandler() { + log.Info("Received HUP signal, log will be reopened…") + log.Reopen() + log.Info("Log reopened by HUP signal") +} + +// printErrorAndExit print error message and exit with exit code 1 +func printErrorAndExit(f string, a ...interface{}) { + terminal.Error(f, a...) + os.Exit(1) +} + +// shutdown stops daemon +func shutdown(code int) { + os.Exit(code) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// genUsage generates usage info +func genUsage() *usage.Info { + info := usage.NewInfo() + + info.AddOption(OPT_CONFIG, "Path to configuration file", "file") + info.AddOption(OPT_NO_COLOR, "Disable colors in output") + info.AddOption(OPT_HELP, "Show this help message") + info.AddOption(OPT_VER, "Show version") + + return info +} + +// genAbout generates info about version +func genAbout(gitRev string) *usage.About { + about := &usage.About{ + App: APP, + Version: VER, + Desc: DESC, + Year: 2009, + Owner: "ESSENTIAL KAOS", + + AppNameColorTag: colorTagApp, + VersionColorTag: colorTagVer, + DescSeparator: "{s}—{!}", + + BugTracker: "https://github.com/essentialkaos/swap-reaper/issues", + License: "Apache License, Version 2.0 ", + } + + if gitRev != "" { + about.Build = "git:" + gitRev + } + + return about +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d304b2d --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/essentialkaos/swap-reaper + +go 1.21 + +require github.com/essentialkaos/ek/v13 v13.5.1 + +require ( + github.com/essentialkaos/depsy v1.3.0 // indirect + golang.org/x/sys v0.25.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aaab24e --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/essentialkaos/check v1.4.0 h1:kWdFxu9odCxUqo1NNFNJmguGrDHgwi3A8daXX1nkuKk= +github.com/essentialkaos/check v1.4.0/go.mod h1:LMKPZ2H+9PXe7Y2gEoKyVAwUqXVgx7KtgibfsHJPus0= +github.com/essentialkaos/depsy v1.3.0 h1:CN7bRgBU2jGTHSkg/Sh38eDUn7cvmaTp2sxFt2HpFeU= +github.com/essentialkaos/depsy v1.3.0/go.mod h1:kpiTAV17dyByVnrbNaMcZt2jRwvuXClUYOzpyJQwtG8= +github.com/essentialkaos/ek/v13 v13.5.1 h1:xkr3d5uAzs69AqI0oKHjjZIsowKdR117AMBv+Dop4Fk= +github.com/essentialkaos/ek/v13 v13.5.1/go.mod h1:KBOtJlrIC2etc/EXvMdbz1JeKmtkuVdK6uRW/ap0OPM= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/swap-reaper.go b/swap-reaper.go new file mode 100644 index 0000000..4808181 --- /dev/null +++ b/swap-reaper.go @@ -0,0 +1,28 @@ +package main + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2024 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + _ "embed" + + DAEMON "github.com/essentialkaos/swap-reaper/daemon" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +//go:embed go.mod +var gomod []byte + +// gitrev is short hash of the latest git commit +var gitrev string + +// ////////////////////////////////////////////////////////////////////////////////// // + +func main() { + DAEMON.Run(gitrev, gomod) +}