commit:     f8058e1a817325da080f43ffbd03e7e67c9ab1ce
Author:     Michał Górny <mgorny <AT> gentoo <DOT> org>
AuthorDate: Sun Nov 16 19:17:33 2025 +0000
Commit:     Michał Górny <mgorny <AT> gentoo <DOT> org>
CommitDate: Sun Nov 16 20:05:31 2025 +0000
URL:        https://gitweb.gentoo.org/proj/steve.git/commit/?id=f8058e1a

Start rewriting using CUSE

Signed-off-by: Michał Górny <mgorny <AT> gentoo.org>

 README.rst     | 100 -------------------------------
 meson.build    |   6 ++
 pyproject.toml |  67 ---------------------
 steve.c        | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 steve.py       | 183 ---------------------------------------------------------
 5 files changed, 186 insertions(+), 350 deletions(-)

diff --git a/README.rst b/README.rst
deleted file mode 100644
index 205fd5f..0000000
--- a/README.rst
+++ /dev/null
@@ -1,100 +0,0 @@
-=====
-steve
-=====
-
-Steve is a trivial standalone jobserver compatible with GNU make.
-Start it via::
-
-    steve ${path_to_fifo}
-
-Then for GNU make / ninja invocations, set::
-
-    MAKEFLAGS="--jobserver-auth=fifo:${path_to_fifo}"
-
-Make sure that your build processes can access the FIFO.  When using
-with Portage, change the ownership to ``portage:portage`` and add it
-to ``SANDBOX_WRITE`` if necessary.
-
-Normally steve runs until explicitly terminated via ^c or SIGTERM.
-
-
-Signals
--------
-Steve responds to the following signals:
-
-SIGINT, SIGTERM
-  Terminate gracefully.
-
-SIGUSR1
-  Print jobserver status.
-
-
-The jobserver protocol and risks
---------------------------------
-Steve implements the variant of GNU make jobserver protocol using
-a named pipe (FIFO).  The scheme is trivial and largely stateless --
-one could hardly call it a server, in fact.  The idea is that steve
-creates a named pipe and write a character for each permitted job to it,
-the so-called "job tokens".  Afterwards, steve just keeps the pipe open,
-while all operations are performed directly by clients.
-
-Clients read job tokens from the pipe to claim them, and write them back
-once the jobs complete.  The total job count is effectively controlled
-by exhausting the job tokens written to the pipe -- the read operation
-blocks until a token is writting back, and then a new job can be
-started.
-
-This can be a blessing but it is also a curse.  Most importantly,
-clients must reliably return job tokens -- any misbehaving client can
-easily consume all job tokens, and effectively stop all builds from
-proceeding.  This can particularly be a case if the client (ninja,
-GNU make) is killed by ``SIGKILL``, as it effectively prevents it
-from running a cleanup routine.  If that happens, one can artificially
-add job tokens back, as per `Adjusting the job count at runtime`_.
-
-Another counter-intuitive fact is that restarting steve does not reset
-the tob tokens for existing clients.  The old named pipe will be
-replaced by a new one.  The existing clients will continue using job
-tokens from the old pipe, while new clients will consume them from
-the new pipe.  This can mean that every steve restart will result
-in the total job count growing linearly, and that builds hanged due
-to all jobs being consumed will remain hanged.
-
-
-Control FIFO
-------------
-Optionally, steve can be started with a control/locking FIFO using::
-
-    steve -c ${path_to_control} ${path_to_fifo}
-
-Steve then opens the read end of the pipe and waits for a client (such
-as ``emerge``) to open the write end before actually writing any job
-tokens.  If multiple processes need to use Steve, they should all hold
-the write end open.  Steve reads the FIFO until EOF, then exits
--- effectively waiting for all locked processes to terminate.
-
-This effectively means that the first process needing steve can start
-a shared instance dynamically, and it will continue running until
-the last process exits.
-
-In the future, the control FIFO may be used to pass explicit commands
-to steve.
-
-
-Adjusting the job count at runtime
-----------------------------------
-Due to the simplicity of the jobserver protocol, it is trivially
-possible to adjust the job count at runtime.
-
-To add more jobs to the jobserver, use ``--add-jobs`` with the same
-jobserver FIFO path::
-
-    steve -a ${jobs} ${path_to_fifo}
-
-To remove them, use ``--remove-jobs``.  This will block until
-all the requested jobs are removed.  If the number is larger than total
-number of jobs, it will block forever.
-
-::
-
-    steve -d ${jobs} ${path_to_fifo}

diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..8f9e5da
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,6 @@
+project('steve', 'c')
+
+fuse3 = dependency('fuse3')
+
+executable('steve', ['steve.c'],
+           dependencies: fuse3)

diff --git a/pyproject.toml b/pyproject.toml
deleted file mode 100644
index 7f4e74a..0000000
--- a/pyproject.toml
+++ /dev/null
@@ -1,67 +0,0 @@
-[build-system]
-requires = ["flit_core >=3.2,<4"]
-build-backend = "flit_core.buildapi"
-
-[project]
-name = "steve"
-authors = [{name = "Michał Górny", email = "[email protected]"}]
-readme = "README.rst"
-dynamic = ["version", "description"]
-license = {text = "GPL-2.0-or-later"}
-requires-python = ">=3.9"
-classifiers = [
-    "Development Status :: 4 - Beta",
-    "Environment :: Console",
-    "Intended Audience :: System Administrators",
-    "License :: OSI Approved :: GNU General Public License v2 or later 
(GPLv2+)",
-    "Operating System :: POSIX",
-    "Programming Language :: Python",
-    "Topic :: Software Development :: Build Tools",
-]
-
-[project.scripts]
-steve = "steve:main"
-
-[project.urls]
-Homepage = "https://gitweb.gentoo.org/proj/steve.git/";
-
-[tool.flit.sdist]
-include = [
-    "COPYING",
-]
-
-[tool.ruff]
-line-length = 80
-
-[tool.ruff.lint]
-extend-select = [
-    "E",
-    "N",
-    "W",
-    "I",
-    "UP",
-    "ANN",
-    "B",
-    "A",
-    "COM",
-    "C4",
-    "EXE",
-    "ISC",
-    "PIE",
-    "PT",
-    "Q",
-    "RSE",
-    "RET",
-    "SLOT",
-    "SIM",
-    "TCH",
-    "ARG",
-    "ERA",
-    "PGH",
-    "FURB",
-    "RUF",
-]
-
-[tool.ruff.lint.flake8-copyright]
-min-file-size = 1
-notice-rgx = "\\(c\\) \\d{4}(-\\d{4})?"

diff --git a/steve.c b/steve.c
new file mode 100644
index 0000000..3002016
--- /dev/null
+++ b/steve.c
@@ -0,0 +1,180 @@
+/* Steve, the jobserver
+ * (c) 2025 Michał Górny
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Inspired by CUSE example and nixos-jobserver (draft):
+ * 
https://github.com/libfuse/libfuse/blob/f58d4c5b0d56116d8870753f6b9d1620ee082709/example/cuse.c
+ * 
https://github.com/RaitoBezarius/nixpkgs/blob/e97220ecf1e8887b949e4e16547bf0334826d076/pkgs/by-name/ni/nixos-jobserver/nixos-jobserver.cpp#L213
+ */
+
+#define FUSE_USE_VERSION 31
+
+#include <cuse_lowlevel.h>
+#include <fuse.h>
+#include <fuse_opt.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+
+static const char *usage =
+"usage: steve [options]\n"
+"\n"
+"options:\n"
+"    --help|-h             print this help message\n"
+"    --jobs=JOBS|-j JOBS   jobs to use (default: nproc)\n"
+"    --verbose|-v          enable verbose logging\n"
+"    -d   -o debug         enable debug output (implies -f)\n"
+"    -f                    foreground operation\n"
+"    -s                    disable multi-threaded operation\n"
+"\n";
+
+struct steve_state {
+       bool verbose;
+       unsigned int jobs;
+       unsigned int tokens;
+};
+
+enum steve_arg_keys {
+       STEVE_HELP,
+       STEVE_VERBOSE,
+};
+
+#define STEVE_OPT(t, p) { t, offsetof(struct steve_state, p), 1 }
+
+static const struct fuse_opt steve_opts[] = {
+       STEVE_OPT("-j %u", jobs),
+       STEVE_OPT("--jobs=%u", jobs),
+       FUSE_OPT_KEY("-v", STEVE_VERBOSE),
+       FUSE_OPT_KEY("--verbose", STEVE_VERBOSE),
+       FUSE_OPT_KEY("-h", STEVE_HELP),
+       FUSE_OPT_KEY("--help", STEVE_HELP),
+       FUSE_OPT_END
+};
+
+static int steve_process_arg(
+       void *userdata, const char *arg, int key, struct fuse_args *outargs)
+{
+       struct steve_state *state = userdata;
+
+       (void) outargs;
+       (void) arg;
+
+       switch (key) {
+       case STEVE_HELP:
+               fprintf(stderr, "%s", usage);
+               return fuse_opt_add_arg(outargs, "-ho");
+       case STEVE_VERBOSE:
+               state->verbose = true;
+               break;
+       default:
+               return 1;
+       }
+
+       return 0;
+}
+
+static void steve_init(void *userdata, struct fuse_conn_info *conn)
+{
+       struct steve_state *state = userdata;
+
+       /* Disable the receiving and processing of FUSE_INTERRUPT requests */
+       conn->no_interrupt = 1;
+
+       state->tokens = state->jobs;
+
+       fprintf(stderr, "steve running on /dev/steve for %d jobs\n", 
state->jobs);
+}
+
+static void steve_open(fuse_req_t req, struct fuse_file_info *fi)
+{
+       fuse_reply_open(req, fi);
+}
+
+static void steve_read(
+       fuse_req_t req, size_t size, off_t off, struct fuse_file_info *fi)
+{
+       const struct fuse_ctx *context = fuse_req_ctx(req);
+       struct steve_state *state = fuse_req_userdata(req);
+
+       if (off != 0) {
+               fuse_reply_err(req, EIO);
+               return;
+       }
+       if (size == 0) {
+               fuse_reply_buf(req, "", 0);
+               return;
+       }
+
+       /* no need to support reading more than one token at a time */
+       if (state->tokens > 0) {
+               state->tokens--;
+               if (state->verbose)
+                       printf("Giving job token to PID %d, %d left\n",
+                                       context->pid, state->tokens);
+               fuse_reply_buf(req, "+", 1);
+               return;
+       }
+
+       if (fi->flags & O_NONBLOCK) {
+               fuse_reply_err(req, EAGAIN);
+               return;
+       }
+
+       /* TODO: implement waiting */
+       fuse_reply_err(req, EIO);
+}
+
+static void steve_write(
+       fuse_req_t req, const char *buf, size_t size, off_t off,
+       struct fuse_file_info *fi)
+{
+       const struct fuse_ctx *context = fuse_req_ctx(req);
+       struct steve_state *state = fuse_req_userdata(req);
+
+       if (off != 0) {
+               fuse_reply_err(req, EIO);
+               return;
+       }
+
+       state->tokens += size;
+       if (state->verbose)
+               printf("PID %d returned %zd tokens, %d available now\n",
+                               context->pid, size, state->tokens);
+       fuse_reply_write(req, size);
+}
+
+static const struct cuse_lowlevel_ops steve_ops = {
+       .init = steve_init,
+       .open = steve_open,
+       .read = steve_read,
+       .write = steve_write,
+};
+
+int main(int argc, char **argv)
+{
+       struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
+       const char *dev_name = "DEVNAME=steve";
+       const char *dev_info_argv[] = { dev_name };
+       struct cuse_info ci = { 0 };
+       struct steve_state state = { 0 };
+       int ret;
+
+       if (fuse_opt_parse(&args, &state, steve_opts, steve_process_arg)) {
+               fprintf(stderr, "failed to parse option\n");
+               fuse_opt_free_args(&args);
+               return 1;
+       }
+
+       ci.dev_info_argc = 1;
+       ci.dev_info_argv = dev_info_argv;
+       if (state.jobs == 0)
+               state.jobs = sysconf(_SC_NPROCESSORS_ONLN);
+
+       ret = cuse_lowlevel_main(args.argc, args.argv, &ci, &steve_ops, &state);
+       fuse_opt_free_args(&args);
+       return ret;
+}

diff --git a/steve.py b/steve.py
deleted file mode 100755
index 5606009..0000000
--- a/steve.py
+++ /dev/null
@@ -1,183 +0,0 @@
-#!/usr/bin/env python
-# (c) 2025 Michał Górny
-# SPDX-License-Identifier: GPL-2.0-or-later
-
-"""steve -- a simple jobserver for Gentoo"""
-# based on 
https://github.com/ninja-build/ninja/blob/master/misc/jobserver_pool.py
-
-from __future__ import annotations
-
-import argparse
-import array
-import contextlib
-import fcntl
-import multiprocessing
-import os
-import select
-import signal
-import termios
-from pathlib import Path
-from typing import TYPE_CHECKING
-
-if TYPE_CHECKING:
-    import io
-    from collections.abc import Generator
-    from types import FrameType
-
-
-__version__ = "0.0.1"
-
-
-def positive_int(value: str) -> int:
-    ret = int(value)
-    if ret <= 0:
-        raise argparse.ArgumentTypeError("must be positive")
-    return ret
-
-
-def exit_sig_handler(signum: int, _: FrameType | None) -> None:
-    print(f"Exiting on {signal.Signals(signum).name}")
-    raise SystemExit(0)
-
-
-def print_state(job_file: io.FileIO) -> None:
-    data_buf = array.array("i", [0])
-    fcntl.ioctl(job_file.fileno(), termios.FIONREAD, data_buf)
-    print(f"Available job tokens: {data_buf[0]}")
-
-
[email protected]
-def hold_fifo(path: Path) -> Generator[Path]:
-    os.mkfifo(path)
-    try:
-        yield path
-    finally:
-        path.unlink()
-
-
[email protected]
-def close_fd(fd: int) -> Generator[int]:
-    try:
-        yield fd
-    finally:
-        os.close(fd)
-
-
-def remove_job_tokens(pipe_fd: int, num: int) -> None:
-    """Remove num job tokens from pipe pipe_fd, open non-blocking"""
-
-    while num > 0:
-        try:
-            removed = len(os.read(pipe_fd, num))
-        except BlockingIOError:
-            # block until more data is available
-            select.select([pipe_fd], [], [])
-        else:
-            num -= removed
-            print(f"{removed} jobs removed, {num} left")
-
-
[email protected]
-def job_cleanup(pipe_fd: int, expected_jobs: int) -> Generator[None]:
-    try:
-        yield
-    finally:
-        print(f"Waiting for {expected_jobs} tokens to be returned")
-        remove_job_tokens(pipe_fd, expected_jobs)
-
-
-def main() -> None:
-    signal.signal(signal.SIGINT, exit_sig_handler)
-    signal.signal(signal.SIGTERM, exit_sig_handler)
-    signal.signal(signal.SIGUSR1, signal.SIG_IGN)
-
-    argp = argparse.ArgumentParser()
-
-    commands = argp.add_mutually_exclusive_group()
-    commands.add_argument(
-        "-a",
-        "--add-jobs",
-        type=positive_int,
-        help="Add the specified number of jobs to the running jobserver",
-    )
-    commands.add_argument(
-        "-d",
-        "--delete-jobs",
-        "--remove-jobs",
-        type=positive_int,
-        help="Remove the specified number of jobs to the running jobserver",
-    )
-
-    argp.add_argument(
-        "-c",
-        "--control-fifo",
-        type=Path,
-        help="Path for the control/lock FIFO",
-    )
-    argp.add_argument(
-        "-j",
-        "--jobs",
-        type=positive_int,
-        help="Number of jobs to allow (default: nproc)",
-    )
-    argp.add_argument(
-        "path",
-        type=Path,
-        help="Path to create the FIFO at",
-    )
-    args = argp.parse_args()
-
-    if args.add_jobs is not None:
-        if args.add_jobs <= 0:
-            argp.error("--add-jobs must be larger than 0")
-        with args.path.open("wb") as job_file:
-            job_file.write(b"." * args.add_jobs)
-        print(f"{args.add_jobs} job tokens added to {args.path}")
-        return
-
-    if args.delete_jobs:
-        with close_fd(
-            os.open(args.path, os.O_RDONLY | os.O_NONBLOCK),
-        ) as job_fd:
-            print(f"Removing {args.delete_jobs} job tokens from {args.path}")
-            remove_job_tokens(job_fd, args.delete_jobs)
-        return
-
-    if args.jobs is None:
-        try:
-            args.jobs = multiprocessing.cpu_count()
-        except NotImplementedError:
-            argp.error("Cannot determine CPU count, please specify --jobs")
-
-    with contextlib.ExitStack() as context_managers:
-        context_managers.enter_context(hold_fifo(args.path))
-        job_read_fd = os.open(args.path, os.O_RDONLY | os.O_NONBLOCK)
-        context_managers.enter_context(close_fd(job_read_fd))
-        job_file = args.path.open("wb")
-        signal.signal(
-            signal.SIGUSR1,
-            lambda _sig, _frame: print_state(job_file),
-        )
-        if args.control_fifo is not None:
-            context_managers.enter_context(hold_fifo(args.control_fifo))
-            print(f"Waiting for clients to open {args.control_fifo}")
-            control_file = args.control_fifo.open("rb")
-
-        job_file.write(b"." * args.jobs)
-        job_file.flush()
-        context_managers.enter_context(job_cleanup(job_read_fd, args.jobs))
-
-        print(f"Started for {args.jobs} jobs")
-        print(f'MAKEFLAGS="--jobserver-auth=fifo:{args.path}"')
-
-        if args.control_fifo is not None:
-            print("Running until the last client closes the control FIFO")
-            control_file.read()
-            print("Control FIFO released, exiting")
-        else:
-            while True:
-                signal.pause()
-
-
-if __name__ == "__main__":
-    main()

Reply via email to