commit:     6231c713ae2813b560473bd7f38904b22b64afd4
Author:     Michał Górny <mgorny <AT> gentoo <DOT> org>
AuthorDate: Sat Nov 29 17:36:35 2025 +0000
Commit:     Michał Górny <mgorny <AT> gentoo <DOT> org>
CommitDate: Sat Nov 29 17:36:51 2025 +0000
URL:        https://gitweb.gentoo.org/proj/steve.git/commit/?id=6231c713

Add initial ioctls

Add ioctls to get token count, get/set job number and get/set min-jobs.

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

 steve.cxx  | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
 stevie.cxx | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++--------
 2 files changed, 172 insertions(+), 20 deletions(-)

diff --git a/steve.cxx b/steve.cxx
index c567a13..eb2c6f4 100644
--- a/steve.cxx
+++ b/steve.cxx
@@ -10,6 +10,7 @@
 
 #define FUSE_USE_VERSION 31
 
+#include <cassert>
 #include <cstddef>
 #include <cstdio>
 #include <cstdlib>
@@ -34,6 +35,7 @@
 #include <fuse.h>
 #include <fuse_opt.h>
 
+#include "steve.h"
 #include "util.hxx"
 
 struct steve_read_waiter {
@@ -69,11 +71,11 @@ struct steve_process {
 
 struct steve_state {
        bool verbose;
-       size_t jobs;
-       size_t min_jobs;
+       uint64_t jobs;
+       uint64_t min_jobs;
        double max_load_avg{-1};  /* < 0 implies no load average */
        double load_avg;
-       size_t tokens;
+       int64_t tokens;
        std::deque<steve_read_waiter> read_waiters;
        std::deque<steve_poll_waiter> poll_waiters;
        std::unordered_map<uint64_t, steve_process> processes;
@@ -93,7 +95,7 @@ enum class steve_token_availability {
 
 static steve_token_availability steve_can_give_token(steve_state *state)
 {
-       if (state->tokens == 0)
+       if (state->tokens <= 0)
                return steve_token_availability::no_tokens;
        if (state->max_load_avg > 0) {
                if (state->jobs < state->min_jobs + state->tokens)
@@ -393,6 +395,88 @@ static void steve_poll(
        fuse_reply_poll(req, events);
 }
 
+static void steve_ioctl(
+       fuse_req_t req, int cmd, void *, fuse_file_info *fi,
+       unsigned flags, const void *in_buf, size_t in_buf_sz, size_t out_buf_sz)
+{
+       steve_state *state = static_cast<steve_state *>(fuse_req_userdata(req));
+       /* FUSE uses the wrong type, sigh */
+       unsigned ioctl_num = cmd;
+
+       if (flags & FUSE_IOCTL_COMPAT) {
+               fuse_reply_err(req, ENOSYS);
+               return;
+       }
+
+       if (state->verbose)
+               std::print(stderr, "PID {} requested ioctl 0x{:08x}\n",
+                               fi->fh, ioctl_num);
+
+       /* TODO: is this generated by fuse? would assert() be better? */
+       if (STEVE_IOC_IS_GET(ioctl_num) && out_buf_sz != sizeof(int64_t)) {
+               fuse_reply_err(req, EINVAL);
+               return;
+       }
+       if (STEVE_IOC_IS_SET(ioctl_num) && in_buf_sz != sizeof(int64_t)) {
+               fuse_reply_err(req, EINVAL);
+               return;
+       }
+
+       int64_t val;
+       if (STEVE_IOC_IS_SET(ioctl_num)) {
+               const int64_t *in_val = static_cast<const int64_t *>(in_buf);
+               if (*in_val < 0 || *in_val >= INT_MAX) {
+                       fuse_reply_err(req, EINVAL);
+                       return;
+               }
+
+               val = *in_val;
+       }
+
+       switch (ioctl_num) {
+               case STEVE_IOC_GET_TOKENS:
+                       val = state->tokens;
+                       fuse_reply_ioctl(req, 0, &val, sizeof(val));
+                       break;
+               case STEVE_IOC_GET_JOBS:
+                       val = state->jobs;
+                       fuse_reply_ioctl(req, 0, &val, sizeof(val));
+                       break;
+               case STEVE_IOC_GET_MIN_JOBS:
+                       val = state->min_jobs;
+                       fuse_reply_ioctl(req, 0, &val, sizeof(val));
+                       break;
+               case STEVE_IOC_SET_JOBS:
+                       if (val == 0)
+                               val = sysconf(_SC_NPROCESSORS_ONLN);
+                       state->tokens += val - state->jobs;
+                       state->jobs = val;
+                       std::print("PID {} set jobs to {}\n", fi->fh, 
state->jobs);
+                       if (state->verbose)
+                               std::print("  new token availability: {}\n", 
state->tokens);
+                       if (state->min_jobs > state->jobs) {
+                               state->min_jobs = state->jobs;
+                               if (state->verbose)
+                                       std::print("  capping min-jobs to 
{}\n", state->min_jobs);
+                       }
+                       fuse_reply_ioctl(req, 0, nullptr, 0);
+                       steve_wake_waiters(state);
+                       break;
+               case STEVE_IOC_SET_MIN_JOBS:
+                       if (static_cast<uint64_t>(val) > state->jobs) {
+                               fuse_reply_err(req, EINVAL);
+                               return;
+                       }
+                       state->min_jobs = val;
+                       std::print("PID {} set min-jobs to {}\n", fi->fh, 
state->min_jobs);
+                       fuse_reply_ioctl(req, 0, nullptr, 0);
+                       steve_wake_waiters(state);
+                       break;
+               default:
+                       fuse_reply_err(req, ENOTTY);
+       }
+}
+
 #pragma GCC diagnostic push
 #pragma GCC diagnostic ignored "-Wmissing-field-initializers"
 static const struct cuse_lowlevel_ops steve_ops = {
@@ -402,6 +486,7 @@ static const struct cuse_lowlevel_ops steve_ops = {
        .read = steve_read,
        .write = steve_write,
        .release = steve_release,
+       .ioctl = steve_ioctl,
        .poll = steve_poll,
 };
 #pragma GCC diagnostic pop
@@ -467,10 +552,8 @@ int main(int argc, char **argv)
                        case 'j':
                        case 'm':
                                {
-                                       char *endptr;
-                                       errno = 0;
-                                       long jobs_arg = strtol(optarg, &endptr, 
10);
-                                       if (*endptr || errno == ERANGE || 
jobs_arg < 0 || jobs_arg > INT_MAX) {
+                                       long jobs_arg;
+                                       if (!arg_to_long(optarg, &jobs_arg)) {
                                                std::print(stderr, "invalid job 
number: {}\n", optarg);
                                                return 1;
                                        }

diff --git a/stevie.cxx b/stevie.cxx
index 523ed0c..2957825 100644
--- a/stevie.cxx
+++ b/stevie.cxx
@@ -7,15 +7,20 @@
  * https://codeberg.org/amonakov/guildmaster/
  */
 
+#include <cassert>
 #include <cerrno>
 #include <cstring>
 #include <print>
+#include <utility>
+#include <vector>
 
 #include <fcntl.h>
 #include <getopt.h>
+#include <sys/ioctl.h>
 #include <sys/wait.h>
 #include <unistd.h>
 
+#include "steve.h"
 #include "util.hxx"
 
 struct token_guard {
@@ -120,30 +125,46 @@ static int run_command(int jobserver_fd, char **argv, 
const char *jobserver_path
 }
 
 static constexpr char stevie_usage[] =
-       "usage: {} [options] <argv>...\n"
-       "\n"
-       "options:\n"
-       "    --help, -h             print this help message\n"
-       "    --version, -V          print version\n"
-       "    --jobserver PATH,\n"
-       "    -j PATH                jobserver FIFO path (default: 
/dev/steve)\n";
+"usage: {0} [options] <argv>...\n"
+"\n"
+"options:\n"
+"    --help, -h             print this help message\n"
+"    --version, -V          print version\n"
+"    --jobserver PATH, -j PATH\n"
+"                           jobserver FIFO path (default: /dev/steve)\n"
+"\n"
+"other actions (executed before the command):\n"
+"    --get-tokens, -t       print available token count\n"
+"    --get-jobs, -j         print total job number\n"
+"    --set-jobs JOBS, -J JOBS\n"
+"                           set total job number\n"
+"    --get-min-jobs, -m     print min-job number\n"
+"    --set-min-jobs JOBS, -M JOBS\n"
+"                           set min-job number\n";
 
 static const struct option stevie_long_opts[] = {
        {"help", no_argument, 0, 'h'},
        {"version", no_argument, 0, 'V'},
-       {"jobserver", required_argument, 0, 'j'},
+       {"jobserver", required_argument, 0, 'p'},
+       {"get-tokens", no_argument, 0, 't'},
+       {"get-jobs", no_argument, 0, 'j'},
+       {"set-jobs", required_argument, 0, 'J'},
+       {"get-min-jobs", no_argument, 0, 'm'},
+       {"set-min-jobs", required_argument, 0, 'M'},
        {},
 };
 
-static const char *stevie_short_opts = "+hVj:";
+static const char *stevie_short_opts = "+hVtjJ:mM:";
 
 int main(int argc, char **argv)
 {
        const char *jobserver_path = "/dev/steve";
 
        int opt;
+       std::vector<std::pair<unsigned long, int64_t>> actions;
        while ((opt = getopt_long(argc, argv, stevie_short_opts,
                                stevie_long_opts, nullptr)) != -1) {
+               unsigned long ioctl_num = 0;
                switch (opt) {
                case 'h':
                        std::print(stevie_usage, argv[0]);
@@ -151,16 +172,45 @@ int main(int argc, char **argv)
                case 'V':
                        std::print("stevie {}\n", STEVE_VERSION);
                        return 0;
-               case 'j':
+               case 'p':
                        jobserver_path = optarg;
                        break;
+               case 't':
+                       ioctl_num = STEVE_IOC_GET_TOKENS;
+                       break;
+               case 'j':
+                       ioctl_num = STEVE_IOC_GET_JOBS;
+                       break;
+               case 'J':
+                       ioctl_num = STEVE_IOC_SET_JOBS;
+                       break;
+               case 'm':
+                       ioctl_num = STEVE_IOC_GET_MIN_JOBS;
+                       break;
+               case 'M':
+                       ioctl_num = STEVE_IOC_SET_MIN_JOBS;
+                       break;
                default:
                        std::print(stderr, stevie_usage, argv[0]);
                        return 1;
                }
+
+               if (ioctl_num != 0) {
+                       if (STEVE_IOC_IS_GET(ioctl_num)) {
+                               actions.emplace_back(ioctl_num, 0);
+                       } else if (STEVE_IOC_IS_SET(ioctl_num)) {
+                               long long_arg;
+                               if (!arg_to_long(optarg, &long_arg)) {
+                                       std::print(stderr, "invalid value: 
{}\n", optarg);
+                                       return 1;
+                               }
+                               actions.emplace_back(ioctl_num, long_arg);
+                       } else
+                               assert(0 && "not reached");
+               }
        }
 
-       if (!argv[optind]) {
+       if (actions.empty() && !argv[optind]) {
                std::print(stderr, "{}: no command provided\n", argv[0]);
                return 1;
        }
@@ -172,5 +222,24 @@ int main(int argc, char **argv)
        }
        fd_guard jobserver_fd_guard{jobserver_fd};
 
-       return run_command(jobserver_fd, &argv[optind], jobserver_path);
+       for (auto &action : actions) {
+               unsigned long ioctl_num = action.first;
+               int64_t ioctl_val = action.second;
+
+               if (ioctl(jobserver_fd, ioctl_num, &ioctl_val) != 0) {
+                       perror("ioctl failed");
+                       return 1;
+               }
+
+               if (STEVE_IOC_IS_GET(ioctl_num))
+                       std::print("{}\n", ioctl_val);
+               else if (STEVE_IOC_IS_SET(ioctl_num))
+                       std::print("ok\n");
+               else
+                       assert(0 && "not reached");
+       }
+
+       if (argv[optind])
+               return run_command(jobserver_fd, &argv[optind], jobserver_path);
+       return 0;
 }

Reply via email to