├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.adoc ├── ZZZ.8.adoc ├── zzz.8.adoc └── zzz.c /.editorconfig: -------------------------------------------------------------------------------- 1 | ; http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = tab 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.{adoc,yml}] 13 | indent_size = 2 14 | indent_style = space 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | 6 | jobs: 7 | build-ubuntu: 8 | name: Build on Ubuntu x86_64 with ${{ matrix.CC }} 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | CC: 13 | - gcc 14 | - clang 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v1 18 | 19 | - name: Install build dependencies 20 | run: sudo apt-get install asciidoctor 21 | 22 | - run: make build CC=${{ matrix.CC }} 23 | 24 | - run: ./build/zzz -V 25 | 26 | - run: make install DESTDIR=dest 27 | 28 | build-alpine: 29 | name: Build on Alpine ${{ matrix.ARCH }} with ${{ matrix.CC }} ${{ matrix.LDFLAGS }} 30 | runs-on: ubuntu-latest 31 | strategy: 32 | matrix: 33 | ARCH: 34 | - x86_64 35 | - aarch64 36 | - armv7 37 | - ppc64le 38 | - riscv64 39 | CC: 40 | - gcc 41 | LDFLAGS: 42 | - '' 43 | - -static -s 44 | steps: 45 | - name: Checkout repository 46 | uses: actions/checkout@v1 47 | with: 48 | fetch-depth: 0 # fetch all history 49 | 50 | - name: Install latest Alpine Linux for ${{ matrix.ARCH }} 51 | uses: jirutka/setup-alpine@v1 52 | with: 53 | arch: ${{ matrix.ARCH }} 54 | branch: ${{ matrix.ARCH == 'riscv64' && 'edge' || 'latest-stable' }} 55 | packages: asciidoctor build-base 56 | 57 | - name: Get version 58 | run: | 59 | GIT_TAG=$(git describe --tags --match 'v*' 2>/dev/null || echo ${GITHUB_REF##*/}) 60 | echo "VERSION=${GIT_TAG#v}" >> $GITHUB_ENV 61 | 62 | - name: Build zzz 63 | run: | 64 | make build CC=${{ matrix.CC }} LDFLAGS="${{ matrix.LDFLAGS }}" VERSION="${{ env.VERSION }}" 65 | ls -lah build/ 66 | file build/zzz 67 | shell: alpine.sh {0} 68 | 69 | - name: zzz -V 70 | run: ./build/zzz -V 71 | shell: alpine.sh {0} 72 | 73 | - name: Create tarball 74 | if: ${{ matrix.LDFLAGS != '' }} 75 | run: | 76 | TARBALL_NAME=zzz-${{ env.VERSION }}-${{ matrix.ARCH }}-unknown-linux 77 | cp LICENSE build/ 78 | mv build $TARBALL_NAME 79 | tar -czf $TARBALL_NAME.tar.gz $TARBALL_NAME 80 | 81 | - name: Upload tarball to artifacts 82 | if: ${{ matrix.LDFLAGS != '' }} 83 | uses: actions/upload-artifact@v2 84 | with: 85 | name: tarballs 86 | path: '*.tar.gz' 87 | 88 | publish: 89 | name: Publish tarballs to Releases 90 | if: ${{ startsWith(github.ref, 'refs/tags/v') && github.event_name != 'pull_request' }} 91 | needs: 92 | - build-alpine 93 | runs-on: ubuntu-20.04 94 | steps: 95 | - name: Download tarballs from artifacts 96 | uses: actions/download-artifact@v2 97 | 98 | - name: Generate checksums.txt 99 | run: shasum -a 256 *.tar.gz | tee checksums.txt 100 | working-directory: tarballs 101 | 102 | - name: Upload tarballs to Releases 103 | uses: softprops/action-gh-release@v1 104 | with: 105 | files: tarballs/* 106 | env: 107 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 108 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | *.html 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2021-present Jakub Jirutka . 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | prefix := $(or $(prefix),$(PREFIX),/usr/local) 2 | mandir := $(prefix)/share/man 3 | sbindir := $(prefix)/sbin 4 | sysconfdir := /etc 5 | 6 | BUILD_DIR := build 7 | BIN_FILES := zzz 8 | MAN_FILES := $(basename $(wildcard *.[1-9].adoc)) 9 | 10 | ASCIIDOCTOR := asciidoctor 11 | INSTALL := install 12 | LN_S := ln -s 13 | GIT := git 14 | SED := sed 15 | 16 | GIT_REV := $(shell test -d .git && git describe --tags --match 'v*' 2>/dev/null) 17 | ifneq ($(GIT_REV),) 18 | VERSION := $(patsubst v%,%,$(GIT_REV)) 19 | endif 20 | 21 | ifeq ($(DEBUG), 1) 22 | CFLAGS := -g -DDEBUG 23 | CFLAGS += -Wall -Wextra -pedantic 24 | ifeq ($(shell $(CC) --version | grep -q clang && echo clang), clang) 25 | CFLAGS += -Weverything -Wno-vla 26 | endif 27 | else 28 | CFLAGS ?= -Os -DNDEBUG 29 | endif 30 | 31 | D = $(BUILD_DIR) 32 | MAKEFILE_PATH = $(lastword $(MAKEFILE_LIST)) 33 | 34 | 35 | all: build 36 | 37 | #: Print list of targets. 38 | help: 39 | @printf '%s\n\n' 'List of targets:' 40 | @$(SED) -En '/^#:.*/{ N; s/^#: (.*)\n([A-Za-z0-9_-]+).*/\2 \1/p }' $(MAKEFILE_PATH) \ 41 | | while read label desc; do printf '%-15s %s\n' "$$label" "$$desc"; done 42 | 43 | .PHONY: help 44 | 45 | #: Build sources (the default target). 46 | build: build-exec build-man 47 | 48 | #: Build executables. 49 | build-exec: $(addprefix $(D)/,$(BIN_FILES)) 50 | 51 | #: Build man pages. 52 | build-man: $(addprefix $(D)/,$(MAN_FILES)) 53 | 54 | #: Remove generated files. 55 | clean: 56 | rm -rf "$(D)" 57 | 58 | .PHONY: build build-exec build-man clean 59 | 60 | #: Install into $DESTDIR. 61 | install: install-conf install-exec install-man 62 | 63 | #: Create directory for hooks in $DESTDIR/$sysconfdir. 64 | install-conf: 65 | $(INSTALL) -d -m755 "$(DESTDIR)$(sysconfdir)/zzz.d" 66 | 67 | #: Install executables into $DESTDIR/$sbindir/. 68 | install-exec: build-exec 69 | $(INSTALL) -D -m755 $(D)/zzz "$(DESTDIR)$(sbindir)/zzz" 70 | $(LN_S) zzz "$(DESTDIR)$(sbindir)/ZZZ" 71 | 72 | #: Install man pages into $DESTDIR/$mandir/man*/. 73 | install-man: build-man 74 | $(INSTALL) -D -m644 -t $(DESTDIR)$(mandir)/man8/ $(addprefix $(D)/,$(filter %.8,$(MAN_FILES))) 75 | 76 | #: Uninstall from $DESTDIR. 77 | uninstall: 78 | rm -f "$(DESTDIR)$(sbindir)/zzz" 79 | rm -f "$(DESTDIR)$(sbindir)/ZZZ" 80 | for name in $(MAN_FILES); do \ 81 | rm -f "$(DESTDIR)$(mandir)/man$${name##*.}/$$name"; \ 82 | done 83 | rmdir "$(DESTDIR)$(sysconfdir)/zzz.d" || true 84 | 85 | .PHONY: install install-conf install-exec install-man uninstall 86 | 87 | #: Update version in zzz.c and README.adoc to $VERSION. 88 | bump-version: 89 | test -n "$(VERSION)" # $$VERSION 90 | $(SED) -E -i "s/^(:version:).*/\1 $(VERSION)/" README.adoc 91 | $(SED) -E -i "s/(#define\s+VERSION\s+).*/\1\"$(VERSION)\"/" zzz.c 92 | 93 | #: Bump version to $VERSION, create release commit and tag. 94 | release: .check-git-clean | bump-version 95 | test -n "$(VERSION)" # $$VERSION 96 | $(GIT) add . 97 | $(GIT) commit -m "Release version $(VERSION)" 98 | $(GIT) tag -s v$(VERSION) -m v$(VERSION) 99 | 100 | .PHONY: build-version release 101 | 102 | $(D)/%.o: %.c | .builddir 103 | $(CC) $(CFLAGS) -std=c11 $(if $(VERSION),-DVERSION='"$(VERSION)"') -o $@ -c $< 104 | 105 | $(D)/%: $(D)/%.o 106 | $(CC) $(LDFLAGS) -o $@ $< 107 | 108 | $(D)/%.8: %.8.adoc | .builddir 109 | $(ASCIIDOCTOR) -b manpage -o $@ $< 110 | 111 | .builddir: 112 | @mkdir -p "$(D)" 113 | 114 | .check-git-clean: 115 | @test -z "$(shell $(GIT) status --porcelain)" \ 116 | || { echo 'You have uncommitted changes!' >&2; exit 1; } 117 | 118 | .PHONY: .builddir .check-git-clean 119 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Zzz… 2 | :proj-name: zzz 3 | :version: 0.2.0 4 | :gh-name: jirutka/{proj-name} 5 | :repology-name: zzz-jirutka 6 | :releases-uri: https://github.com/{gh-name}/releases/download/v{version} 7 | 8 | ifdef::env-github[] 9 | image:https://github.com/{gh-name}/workflows/CI/badge.svg[Binaries Workflow, link=https://github.com/{gh-name}/actions?query=workflow%3A%22CI%22] 10 | image:https://repology.org/badge/tiny-repos/{repology-name}.svg[Packaging status, link=https://repology.org/project/{repology-name}] 11 | endif::env-github[] 12 | 13 | A simple program to suspend or hibernate your computer. 14 | It supports hooks before and after suspending. 15 | 16 | Refer to link:zzz.8.adoc[zzz(8)] for usage information. 17 | 18 | 19 | == Requirements 20 | 21 | .*Runtime*: 22 | * Linux system with `/sys/power/state` and optionally `/sys/power/disk` 23 | 24 | .*Build*: 25 | * C compiler and linker supporting at least C99 (tested with clang and gcc) 26 | * https://www.gnu.org/software/make/[GNU Make] 27 | * http://asciidoctor.org/[Asciidoctor] (for building man pages) 28 | 29 | 30 | == Installation 31 | 32 | === On Alpine Linux 33 | 34 | Install package https://pkgs.alpinelinux.org/packages?name={proj-name}[{proj-name}] on Alpine Linux v3.15 or later: 35 | 36 | [source, sh, subs="+attributes"] 37 | apk add {proj-name} 38 | 39 | 40 | === On Arch Linux 41 | 42 | Install package https://aur.archlinux.org/packages/{proj-name}[{proj-name}] from AUR: 43 | 44 | [source, sh, subs="+attributes"] 45 | yay -S {proj-name} 46 | 47 | Or use another AUR helper. 48 | 49 | 50 | === Using Pre-Built Binary 51 | 52 | {releases-uri}/{proj-name}-{version}-x86_64-unknown-linux.tar.gz[[x86_64]] 53 | {releases-uri}/{proj-name}-{version}-aarch64-unknown-linux.tar.gz[[aarch64]] 54 | {releases-uri}/{proj-name}-{version}-armv7-unknown-linux.tar.gz[[armv7]] 55 | {releases-uri}/{proj-name}-{version}-ppc64le-unknown-linux.tar.gz[[ppc64le]] 56 | {releases-uri}/{proj-name}-{version}-riscv64-unknown-linux.tar.gz[[riscv64]] 57 | 58 | . Download and extract release tarball for your CPU architecture (pick the right link from the list above): 59 | + 60 | [source, sh, subs="verbatim, attributes"] 61 | ---- 62 | curl -sSLO {releases-uri}/{proj-name}-{version}-x86_64-unknown-linux.tar.gz 63 | curl -sSL {releases-uri}/checksums.txt | sha256sum -c --ignore-missing 64 | tar -xzf {proj-name}-{version}-*.tar.gz 65 | ---- 66 | 67 | . Install `{proj-name}` somewhere on your `PATH`, e.g. `/usr/local/bin`: 68 | + 69 | [source, sh, subs="verbatim, attributes"] 70 | install -m 755 {proj-name}-{version}-*/{proj-name} /usr/local/bin/ 71 | 72 | All binaries are statically linked with http://www.musl-libc.org/[musl libc], so they work on every Linux system (distro) regardless of used libc. 73 | 74 | 75 | === From Source Tarball 76 | 77 | [source, sh, subs="+attributes"] 78 | ---- 79 | wget https://github.com/{gh-name}/archive/v{version}/{proj-name}-{version}.tar.gz 80 | tar -xzf {proj-name}-{version}.tar.gz 81 | cd {proj-name}-{version} 82 | 83 | make build 84 | make install DESTDIR=/ prefix=/usr/local 85 | ---- 86 | 87 | 88 | == Credits 89 | 90 | This program is inspired from https://man.voidlinux.org/zzz.8[zzz(8)] (https://github.com/void-linux/void-runit/blob/master/zzz[source]) in Void Linux written by Leah Neukirchen. 91 | 92 | 93 | == License 94 | 95 | This project is licensed under http://opensource.org/licenses/MIT/[MIT License]. 96 | For the full text of the license, see the link:LICENSE[LICENSE] file. 97 | -------------------------------------------------------------------------------- /ZZZ.8.adoc: -------------------------------------------------------------------------------- 1 | zzz.8.adoc -------------------------------------------------------------------------------- /zzz.8.adoc: -------------------------------------------------------------------------------- 1 | = zzz(8) 2 | :doctype: manpage 3 | :repo-uri: https://github.com/jirutka/zzz 4 | :issues-uri: {repo-uri}/issues 5 | 6 | == NAME 7 | 8 | zzz, ZZZ - suspend or hibernate your computer 9 | 10 | 11 | == SYNOPSIS 12 | 13 | *zzz* [-v] [-n|s|S|z|Z|H|X|R|V|h] + 14 | *ZZZ* [-v] [-n|s|S|z|Z|H|X|R|V|h] 15 | 16 | 17 | == DESCRIPTION 18 | 19 | *zzz* is a simple program to suspend or hibernate your computer. 20 | It supports hooks before and after suspending. 21 | 22 | 23 | == OPTIONS 24 | 25 | *-n*:: 26 | Dry-run mode. 27 | Instead of performing an ACPI action, *zzz* will just sleep for a few seconds. 28 | 29 | *-s*, *-S*:: 30 | Enter low-power idle mode (ACPI S1, kernel name "`freeze`"). 31 | 32 | *-z*:: 33 | Enter suspend to RAM mode (ACPI S3, kernel name "`mem`"). 34 | This is the default for *zzz*. 35 | 36 | *-Z*:: 37 | Enter hibernate to disk mode (ACPI S4, kernel name "`disk`") and power off. 38 | This is the default for *ZZZ*. 39 | 40 | *-H*:: 41 | Enter hibernate to disk mode and suspend. 42 | This is also know as suspend-hybrid. 43 | 44 | *-X*:: 45 | Enter hibernate to disk mode and shutdown. 46 | This can be used when ACPI S4 mode causes issues. 47 | 48 | *-R*:: 49 | Enter hibernate to disk mode and reboot. 50 | This can be used to switch operating systems. 51 | 52 | *-v*:: 53 | Be verbose. 54 | 55 | *-V*:: 56 | Print program name & version and exit. 57 | 58 | *-h*:: 59 | Print help message and exit. 60 | 61 | 62 | == FILES 63 | 64 | /etc/zzz.d/*:: 65 | Hook scripts found in this directory are executed before/after the system is suspended/resumed by *zzz*. 66 | 67 | /etc/zzz.d/suspend/*:: 68 | Hook scripts found in this directory are executed before the system is suspended by *zzz*. 69 | This directory is supported for compatibility with `zzz(8)` on Void Linux. 70 | 71 | /etc/zzz.d/resume/*:: 72 | Hook scripts found in this directory are executed after the system is resumed. 73 | This directory is supported for compatibility with `zzz(8)` on Void Linux. 74 | 75 | The hook script is a regular file (or a symlink) owned by root, executable by the owner and not writeable by others. 76 | Any other files found in the aforesaid directories are ignored. 77 | 78 | The hook scripts are executed sequentially in alphabetic order with two arguments: 79 | 80 | . "`pre`" (before suspend), or "`post`" (after resume), 81 | . the same as *ZZZ_MODE* (see below). 82 | 83 | And the following environment variables: 84 | 85 | ZZZ_MODE:: 86 | The selected suspend mode; one of "`hibernate`", "`noop`", "`standby`", or "`suspend`". 87 | 88 | ZZZ_HIBERNATE_MODE:: 89 | The selected hibernate mode: "`platform`", "`reboot`", "`shutdown`", or "`suspend`". 90 | 91 | 92 | == EXIT CODES 93 | 94 | * *0* -- Clean exit, no error has encountered. 95 | * *1* -- General error. 96 | * *10* -- Invalid usage. 97 | * *11* -- The requested sleep state or hibernation mode is not supported or you don`'t have insufficient privileges. 98 | * *12* -- Unable to obtain lock; another instance of *zzz* is running. 99 | * *20* -- Failed to put system to sleep. 100 | * *21* -- Some hook script exited with a non-zero code. Note that *zzz* does not stop when a hook script fails. 101 | 102 | 103 | == LOGGING 104 | 105 | Information and debug messages are printed to STDOUT, error messages are printed to STDERR. 106 | All messages are also logged to syslog with ident string "`zzz`" and facility code 1 (user). 107 | 108 | Debug messages are not printed and logged unless *-v* is specified. 109 | 110 | 111 | == HISTORY 112 | 113 | This program is inspired from *zzz(8)* script in Void Linux written by Leah Neukirchen. 114 | 115 | 116 | == AUTHORS 117 | 118 | Jakub Jirutka 119 | 120 | 121 | == REPORTING BUGS 122 | 123 | Report bugs to the project`'s issue tracker at {issues-uri}. 124 | -------------------------------------------------------------------------------- /zzz.c: -------------------------------------------------------------------------------- 1 | // vim: set ts=4: 2 | // Copyright 2021 - present, Jakub Jirutka . 3 | // SPDX-License-Identifier: MIT 4 | #define _POSIX_C_SOURCE 200809L 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #define PROGNAME "zzz" 24 | 25 | #ifndef VERSION 26 | #define VERSION "0.2.0" 27 | #endif 28 | 29 | #ifndef ZZZ_HOOKS_DIR 30 | #define ZZZ_HOOKS_DIR "/etc/zzz.d" 31 | #endif 32 | 33 | #ifndef ZZZ_LOCK_FILE 34 | #define ZZZ_LOCK_FILE "/tmp/zzz.lock" 35 | #endif 36 | 37 | #define FLAG_VERBOSE 0x0001 38 | 39 | #define ERR_GENERAL 1 40 | #define ERR_WRONG_USAGE 10 41 | #define ERR_UNSUPPORTED 11 42 | #define ERR_LOCK 12 43 | #define ERR_SUSPEND 20 44 | #define ERR_HOOK 21 45 | 46 | #define RC_OK 0 47 | #define RC_ERR -1 48 | 49 | #define log_err(format, ...) \ 50 | do { \ 51 | fprintf(stderr, PROGNAME ": ERROR: " format "\n", __VA_ARGS__); \ 52 | syslog(LOG_ERR, format, __VA_ARGS__); \ 53 | } while (0) 54 | 55 | #define log_errno(format, ...) \ 56 | log_err(format ": %s", __VA_ARGS__, strerror(errno)) 57 | 58 | #define log_info(format, ...) \ 59 | do { \ 60 | syslog(LOG_INFO, format, __VA_ARGS__); \ 61 | printf(format "\n", __VA_ARGS__); \ 62 | } while (0) 63 | 64 | #define log_debug(format, ...) \ 65 | if (flags & FLAG_VERBOSE) { \ 66 | syslog(LOG_DEBUG, format, __VA_ARGS__); \ 67 | printf(format "\n", __VA_ARGS__); \ 68 | } 69 | 70 | 71 | extern char **environ; 72 | 73 | static const char *HELP_MSG = 74 | "Usage: " PROGNAME " [-v] [-n|s|S|z|Z|H|X|R|V|h]\n" 75 | "\n" 76 | "Suspend or hibernate the system.\n" 77 | "\n" 78 | "Options:\n" 79 | " -n Dry run (sleep for 5s instead of suspend/hibernate).\n" 80 | " -s Low-power idle (ACPI S1).\n" 81 | " -S Deprecated alias for -s.\n" 82 | " -z Suspend to RAM (ACPI S3). [default for zzz(8)]\n" 83 | " -Z Hibernate to disk & power off (ACPI S4). [default for ZZZ(8)]\n" 84 | " -H Hibernate to disk & suspend (aka suspend-hybrid).\n" 85 | " -X Hibernate to disk & shutdown.\n" 86 | " -R Hibernate to disk & reboot.\n" 87 | " -v Be verbose.\n" 88 | " -V Print program name & version and exit.\n" 89 | " -h Show this message and exit.\n" 90 | "\n" 91 | "Homepage: https://github.com/jirutka/zzz\n"; 92 | 93 | static unsigned int flags = 0; 94 | 95 | 96 | static bool str_empty (const char *str) { 97 | return str == NULL || str[0] == '\0'; 98 | } 99 | 100 | static bool str_equal (const char *str1, const char *str2) { 101 | return strcmp(str1, str2) == 0; 102 | } 103 | 104 | static bool str_ends_with (const char *str, const char *suffix) { 105 | int diff = strlen(str) - strlen(suffix); 106 | return diff > 0 && str_equal(&str[diff], suffix); 107 | } 108 | 109 | static int run_hook (const char *path, const char **argv) { 110 | posix_spawn_file_actions_t factions; 111 | int rc = RC_ERR; 112 | 113 | (void) posix_spawn_file_actions_init(&factions); 114 | (void) posix_spawn_file_actions_addclose(&factions, STDIN_FILENO); 115 | 116 | log_debug("Executing hook script: %s", path); 117 | 118 | argv[0] = path; // XXX: mutates input argument! 119 | pid_t pid; 120 | if ((rc = posix_spawn(&pid, path, &factions, 0, (char *const *)argv, environ)) != 0) { 121 | log_errno("Unable to execute hook script %s", path); 122 | goto done; 123 | } 124 | 125 | int status; 126 | (void) waitpid(pid, &status, 0); 127 | 128 | if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { 129 | rc = WEXITSTATUS(status); 130 | log_err("Hook script %s exited with code %d", path, rc); 131 | goto done; 132 | } 133 | rc = RC_OK; 134 | 135 | done: 136 | posix_spawn_file_actions_destroy(&factions); 137 | argv[0] = NULL; // XXX: mutates input argument! 138 | 139 | return rc; 140 | } 141 | 142 | static int filter_hook_script (const struct dirent *entry) { 143 | struct stat sb; 144 | 145 | return stat(entry->d_name, &sb) >= 0 146 | && S_ISREG(sb.st_mode) // is regular file 147 | && sb.st_mode & S_IXUSR // is executable by owner 148 | && !(sb.st_mode & S_IWOTH) // is not writable by others 149 | && sb.st_uid == 0; // is owned by root 150 | } 151 | 152 | static int run_hooks (const char *dirpath, const char **argv) { 153 | struct dirent **entries = NULL; 154 | int rc = RC_OK; 155 | 156 | if (chdir(dirpath) < 0) { 157 | errno = 0; 158 | goto done; 159 | } 160 | int n = 0; 161 | if ((n = scandir(".", &entries, filter_hook_script, alphasort)) < 0) { 162 | log_errno("%s", dirpath); 163 | rc = RC_ERR; 164 | goto done; 165 | } 166 | 167 | char path[PATH_MAX] = "\0"; 168 | for (int i = 0; i < n; i++) { 169 | (void) snprintf(path, sizeof(path), "%s/%s", dirpath, entries[i]->d_name); 170 | 171 | if (run_hook(path, argv) != RC_OK) { 172 | rc = RC_ERR; 173 | } 174 | } 175 | 176 | done: 177 | if (entries) free(entries); 178 | (void) chdir("/"); 179 | 180 | return rc; 181 | } 182 | 183 | static int check_sleep_mode (const char *filepath, const char *mode) { 184 | FILE *fp = NULL; 185 | int rc = RC_ERR; 186 | 187 | if (access(filepath, W_OK) < 0) { 188 | log_errno("%s", filepath); 189 | goto done; 190 | } 191 | 192 | char line[64] = "\0"; 193 | if ((fp = fopen(filepath, "r")) == NULL || fgets(line, sizeof(line), fp) == NULL) { 194 | log_errno("Failed to read %s", filepath); 195 | goto done; 196 | } 197 | 198 | // XXX: This is sloppy... 199 | if (strstr(line, mode) != NULL) { 200 | rc = RC_OK; 201 | } 202 | 203 | done: 204 | if (fp) fclose(fp); 205 | return rc; 206 | } 207 | 208 | static int file_write (const char *filepath, const char *data) { 209 | FILE *fp = NULL; 210 | int rc = RC_ERR; 211 | 212 | if ((fp = fopen(filepath, "w")) == NULL) { 213 | log_errno("%s", filepath); 214 | goto done; 215 | } 216 | (void) setvbuf(fp, NULL, _IONBF, 0); // disable buffering 217 | 218 | if (fputs(data, fp) < 0) { 219 | log_errno("%s", filepath); 220 | goto done; 221 | } 222 | rc = RC_OK; 223 | 224 | done: 225 | if (fp) fclose(fp); 226 | return rc; 227 | } 228 | 229 | static int execute (const char* zzz_mode, const char* sleep_state, const char* hibernate_mode) { 230 | int lock_fd = -1; 231 | int rc = EXIT_SUCCESS; 232 | 233 | // Check if we can fulfil the request. 234 | if (!str_empty(sleep_state) && check_sleep_mode("/sys/power/state", sleep_state) < 0) { 235 | return ERR_UNSUPPORTED; 236 | } 237 | if (!str_empty(hibernate_mode) && check_sleep_mode("/sys/power/disk", hibernate_mode) < 0) { 238 | return ERR_UNSUPPORTED; 239 | } 240 | 241 | // Obtain exclusive lock. 242 | if ((lock_fd = open(ZZZ_LOCK_FILE, O_CREAT | O_RDWR | O_CLOEXEC, 0600)) < 0) { 243 | log_errno("Failed to write %s", ZZZ_LOCK_FILE); 244 | return ERR_LOCK; 245 | } 246 | if (flock(lock_fd, LOCK_EX | LOCK_NB) < 0) { 247 | log_err("%s", "Another instance of zzz is running"); 248 | return ERR_LOCK; 249 | } 250 | 251 | // The first element will be replaced in run_hook() with the script path. 252 | const char *hook_args[] = { NULL, "pre", zzz_mode, NULL }; 253 | 254 | if (run_hooks(ZZZ_HOOKS_DIR, hook_args) < 0) { 255 | rc = ERR_HOOK; 256 | } 257 | // For compatibility with zzz on Void Linux. 258 | if (run_hooks(ZZZ_HOOKS_DIR "/suspend", hook_args) < 0) { 259 | rc = ERR_HOOK; 260 | } 261 | 262 | if (!str_empty(hibernate_mode) && file_write("/sys/power/disk", hibernate_mode) < 0) { 263 | rc = ERR_SUSPEND; 264 | goto done; 265 | } 266 | 267 | log_info("Going to %s (%s)", zzz_mode, sleep_state); 268 | 269 | if (str_empty(sleep_state)) { 270 | sleep(5); 271 | 272 | } else if (file_write("/sys/power/state", sleep_state) < 0) { 273 | log_err("Failed to %s system", zzz_mode); 274 | rc = ERR_SUSPEND; 275 | goto done; 276 | } 277 | 278 | log_info("System resumed from %s (%s)", zzz_mode, sleep_state); 279 | 280 | hook_args[1] = "post"; 281 | if (run_hooks(ZZZ_HOOKS_DIR, hook_args) < 0) { 282 | rc = ERR_HOOK; 283 | } 284 | // For compatibility with zzz on Void Linux. 285 | if (run_hooks(ZZZ_HOOKS_DIR "/resume", hook_args) < 0) { 286 | rc = ERR_HOOK; 287 | } 288 | 289 | done: 290 | // Release lock. 291 | if (unlink(ZZZ_LOCK_FILE) < 0) { 292 | log_errno("Failed to remove lock file %s", ZZZ_LOCK_FILE); 293 | rc = ERR_GENERAL; 294 | } 295 | if (flock(lock_fd, LOCK_UN) < 0) { 296 | log_errno("Failed to release lock on %s", ZZZ_LOCK_FILE); 297 | rc = ERR_GENERAL; 298 | } 299 | (void) close(lock_fd); 300 | 301 | return rc; 302 | } 303 | 304 | int main (int argc, char **argv) { 305 | char *zzz_mode = "suspend"; 306 | char *sleep_state = "mem"; 307 | char *hibernate_mode = ""; 308 | 309 | if (str_equal(argv[0], "ZZZ") || str_ends_with(argv[0], "/ZZZ")) { 310 | zzz_mode = "hibernate"; 311 | sleep_state = "disk"; 312 | hibernate_mode = "platform"; 313 | } 314 | 315 | int optch; 316 | opterr = 0; // don't print implicit error message on unrecognized option 317 | while ((optch = getopt(argc, argv, "nsSzHRXZvhV")) != -1) { 318 | switch (optch) { 319 | case -1: 320 | break; 321 | case 'n': 322 | zzz_mode = "noop"; 323 | sleep_state = ""; 324 | hibernate_mode = ""; 325 | break; 326 | case 's': 327 | case 'S': 328 | zzz_mode = "standby"; 329 | sleep_state = "freeze"; 330 | hibernate_mode = ""; 331 | break; 332 | case 'z': 333 | zzz_mode = "suspend"; 334 | sleep_state = "mem"; 335 | hibernate_mode = ""; 336 | break; 337 | case 'H': 338 | zzz_mode = "hibernate"; 339 | sleep_state = "disk"; 340 | hibernate_mode = "suspend"; 341 | break; 342 | case 'R': 343 | zzz_mode = "hibernate"; 344 | sleep_state = "disk"; 345 | hibernate_mode = "reboot"; 346 | break; 347 | case 'X': 348 | zzz_mode = "hibernate"; 349 | sleep_state = "disk"; 350 | hibernate_mode = "shutdown"; 351 | break; 352 | case 'Z': 353 | zzz_mode = "hibernate"; 354 | sleep_state = "disk"; 355 | hibernate_mode = "platform"; 356 | break; 357 | case 'v': 358 | flags |= FLAG_VERBOSE; 359 | break; 360 | case 'h': 361 | printf("%s", HELP_MSG); 362 | return EXIT_SUCCESS; 363 | case 'V': 364 | puts(PROGNAME " " VERSION); 365 | return EXIT_SUCCESS; 366 | default: 367 | fprintf(stderr, "%1$s: Invalid option: -%2$c (see %1$s -h)\n", PROGNAME, optopt); 368 | return ERR_WRONG_USAGE; 369 | } 370 | } 371 | 372 | (void) chdir("/"); 373 | 374 | // Clear environment variables. 375 | environ = NULL; 376 | 377 | // Set environment for hooks. 378 | setenv("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 1); 379 | setenv("ZZZ_MODE", zzz_mode, 1); 380 | setenv("ZZZ_HIBERNATE_MODE", hibernate_mode, 1); 381 | 382 | // Open connection to syslog. 383 | openlog(PROGNAME, LOG_PID, LOG_USER); 384 | 385 | return execute(zzz_mode, sleep_state, hibernate_mode); 386 | } 387 | --------------------------------------------------------------------------------