From ea32d7109eb055b12bba9e83fb37c4bfe7be8e3a Mon Sep 17 00:00:00 2001 From: Nikolay Govorov Date: Tue, 31 Mar 2026 01:14:18 +0100 Subject: Prepare worker binary --- README.md | 13 ++++++++ Taskfile.yml | 3 ++ cmd/mirumd/main.go | 40 +++++++---------------- cmd/mirumw/main.go | 54 +++++++++++++++++++++++++++++++ internal/supervisor/supervisor.go | 52 +++++++++++++++++++++++++++++ internal/supervisor/systemd.go | 54 +++++++++++++++++++++++++++++++ nfpm.yaml | 35 +++++++++++++++++--- pkg/mirumd.service | 6 ++-- pkg/mirumw-default.yaml | 4 +++ pkg/mirumw@.service | 48 +++++++++++++++++++++++++++ pkg/scripts/postinstall.sh | 5 ++- pkg/scripts/preinstall.sh | 19 +++++------ pkg/scripts/preremove.sh | 5 ++- 13 files changed, 288 insertions(+), 50 deletions(-) create mode 100644 cmd/mirumw/main.go create mode 100644 internal/supervisor/supervisor.go create mode 100644 internal/supervisor/systemd.go create mode 100644 pkg/mirumw-default.yaml create mode 100644 pkg/mirumw@.service diff --git a/README.md b/README.md index 4de2c4e..622bc02 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,11 @@ curl -fsSL https://dl.mirum.dev/public.gpg | sudo gpg --dearmor -o /usr/share/ke echo "deb [signed-by=/usr/share/keyrings/mirum.gpg] https://dl.mirum.dev/apt/ nightly main" | sudo tee /etc/apt/sources.list.d/mirum.list sudo apt update && sudo apt install mirum +# Start the server sudo systemctl enable --now mirumd + +# Start a worker (optional, can run on a different host) +sudo systemctl enable --now mirumw@default ``` **Fedora/RHEL:** @@ -28,7 +32,12 @@ sudo dnf config-manager addrepo --from-repofile=https://dl.mirum.dev/rpm/nightly sudo curl -o /etc/yum.repos.d/mirum-nightly.repo https://dl.mirum.dev/rpm/nightly/mirum-nightly.repo sudo dnf install mirum + +# Start the server sudo systemctl enable --now mirumd + +# Start a worker (optional, can run on a different host) +sudo systemctl enable --now mirumw@default ``` **openSUSE:** @@ -39,7 +48,11 @@ sudo zypper addrepo https://dl.mirum.dev/rpm/nightly/ mirum-nightly sudo zypper refresh sudo zypper install mirum +# Start the server sudo systemctl enable --now mirumd + +# Start a worker (optional, can run on a different host) +sudo systemctl enable --now mirumw@default ``` ## License diff --git a/Taskfile.yml b/Taskfile.yml index 8693c22..259fb37 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -14,6 +14,7 @@ tasks: cmds: - mkdir -p {{.BUILD_DIR}} - go build -o {{.BUILD_DIR}}/mirumd ./cmd/mirumd + - go build -o {{.BUILD_DIR}}/mirumw ./cmd/mirumw package: desc: Build deb/rpm packages for all architectures @@ -27,7 +28,9 @@ tasks: - for: { var: ARCHES } cmd: | GOARCH={{.ITEM}} go build -o {{.BUILD_DIR}}/mirumd-linux-{{.ITEM}} ./cmd/mirumd + GOARCH={{.ITEM}} go build -o {{.BUILD_DIR}}/mirumw-linux-{{.ITEM}} ./cmd/mirumw cp {{.BUILD_DIR}}/mirumd-linux-{{.ITEM}} {{.BUILD_DIR}}/mirumd + cp {{.BUILD_DIR}}/mirumw-linux-{{.ITEM}} {{.BUILD_DIR}}/mirumw ARCH={{.ITEM}} nfpm package --packager deb --target {{.DIST_DIR}}/ ARCH={{.ITEM}} nfpm package --packager rpm --target {{.DIST_DIR}}/ diff --git a/cmd/mirumd/main.go b/cmd/mirumd/main.go index 19afd6a..f5ee0d5 100644 --- a/cmd/mirumd/main.go +++ b/cmd/mirumd/main.go @@ -19,15 +19,13 @@ import ( "net/url" "os" "os/exec" - "os/signal" "path/filepath" - "strconv" "strings" - "syscall" "time" + "mrdimidium/mirum/internal/supervisor" + "github.com/coreos/go-systemd/v22/activation" - "github.com/coreos/go-systemd/v22/daemon" "go.starlark.net/starlark" "gopkg.in/yaml.v3" ) @@ -301,21 +299,21 @@ func main() { srv := &http.Server{Handler: mux} - go func() { - sig := make(chan os.Signal, 1) - signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT) - <-sig + sup := supervisor.Detect() + ctx := sup.WaitForStop(context.Background()) + go func() { + <-ctx.Done() slog.Info("shutting down") - daemon.SdNotify(false, daemon.SdNotifyStopping) + sup.Stopping() - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - srv.Shutdown(ctx) + srv.Shutdown(shutdownCtx) }() - daemon.SdNotify(false, daemon.SdNotifyReady) - go watchdog() + sup.Ready() + go sup.StartWatchdog() if err := srv.Serve(ln); err != http.ErrServerClosed { fmt.Fprintln(os.Stderr, err) @@ -330,19 +328,3 @@ func socketActivationListener() (net.Listener, error) { } return net.Listen("tcp", cfg.Address) } - -func watchdog() { - usecStr := os.Getenv("WATCHDOG_USEC") - if usecStr == "" { - return - } - usec, err := strconv.ParseInt(usecStr, 10, 64) - if err != nil || usec <= 0 { - return - } - interval := time.Duration(usec) * time.Microsecond / 2 - for { - daemon.SdNotify(false, daemon.SdNotifyWatchdog) - time.Sleep(interval) - } -} diff --git a/cmd/mirumw/main.go b/cmd/mirumw/main.go new file mode 100644 index 0000000..194abfa --- /dev/null +++ b/cmd/mirumw/main.go @@ -0,0 +1,54 @@ +// Copyright (c) 2026 Nikolay Govorov +// SPDX-License-Identifier: AGPL-3.0-or-later + +package main + +import ( + "context" + "flag" + "fmt" + "log/slog" + "os" + + "mrdimidium/mirum/internal/supervisor" + + "gopkg.in/yaml.v3" +) + +type config struct { + Server string `yaml:"server"` +} + +var cfg = config{ + Server: "localhost:2026", +} + +var configFile = flag.String("config", "", "path to config file") + +func main() { + flag.Parse() + + if *configFile != "" { + data, err := os.ReadFile(*configFile) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if err := yaml.Unmarshal(data, &cfg); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + } + + sup := supervisor.Detect() + ctx := sup.WaitForStop(context.Background()) + + slog.Info("connecting", "server", cfg.Server) + + sup.Ready() + go sup.StartWatchdog() + + <-ctx.Done() + slog.Info("shutting down") + sup.Stopping() +} diff --git a/internal/supervisor/supervisor.go b/internal/supervisor/supervisor.go new file mode 100644 index 0000000..2e99265 --- /dev/null +++ b/internal/supervisor/supervisor.go @@ -0,0 +1,52 @@ +// Copyright (c) 2026 Nikolay Govorov +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Package supervisor provides a platform-agnostic interface for process +// supervision (systemd, launchd, Windows Services, etc.). +// +// On systems without a recognized supervisor the functions are no-ops. +package supervisor + +import ( + "context" + "os/signal" + "syscall" +) + +// Supervisor communicates lifecycle events to the process supervisor +// and handles platform-specific shutdown signals. +type Supervisor interface { + // Ready signals that the service has started and is ready to serve. + Ready() + + // Stopping signals that the service has begun graceful shutdown. + Stopping() + + // StartWatchdog begins sending periodic keepalive pings. + // Blocks forever; call as a goroutine. Returns immediately if the + // supervisor does not require keepalives. + StartWatchdog() + + // WaitForStop blocks until the supervisor or OS requests shutdown. + WaitForStop(ctx context.Context) context.Context +} + +// Detect returns a Supervisor for the current platform. +func Detect() Supervisor { + if n := detectSystemd(); n != nil { + return n + } + return &noop{} +} + +type noop struct{} + +func (*noop) Ready() {} +func (*noop) Stopping() {} +func (*noop) StartWatchdog() {} + +func (*noop) WaitForStop(ctx context.Context) context.Context { + ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) + _ = stop + return ctx +} diff --git a/internal/supervisor/systemd.go b/internal/supervisor/systemd.go new file mode 100644 index 0000000..88f1f94 --- /dev/null +++ b/internal/supervisor/systemd.go @@ -0,0 +1,54 @@ +// Copyright (c) 2026 Nikolay Govorov +// SPDX-License-Identifier: AGPL-3.0-or-later + +package supervisor + +import ( + "context" + "os" + "os/signal" + "strconv" + "syscall" + "time" + + "github.com/coreos/go-systemd/v22/daemon" +) + +type systemd struct{} + +func detectSystemd() Supervisor { + if os.Getenv("NOTIFY_SOCKET") == "" { + return nil + } + return &systemd{} +} + +func (*systemd) WaitForStop(ctx context.Context) context.Context { + ctx, stop := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT) + _ = stop + return ctx +} + +func (*systemd) Ready() { + daemon.SdNotify(false, daemon.SdNotifyReady) +} + +func (*systemd) Stopping() { + daemon.SdNotify(false, daemon.SdNotifyStopping) +} + +func (*systemd) StartWatchdog() { + usecStr := os.Getenv("WATCHDOG_USEC") + if usecStr == "" { + return + } + usec, err := strconv.ParseInt(usecStr, 10, 64) + if err != nil || usec <= 0 { + return + } + interval := time.Duration(usec) * time.Microsecond / 2 + for { + daemon.SdNotify(false, daemon.SdNotifyWatchdog) + time.Sleep(interval) + } +} diff --git a/nfpm.yaml b/nfpm.yaml index beea2ff..f19f0a3 100644 --- a/nfpm.yaml +++ b/nfpm.yaml @@ -23,25 +23,50 @@ contents: file_info: mode: 0755 + - src: build/mirumw + dst: /usr/local/bin/mirumw + file_info: + mode: 0755 + - src: pkg/mirumd.yaml - dst: /etc/mirumd.yaml + dst: /etc/mirum/mirumd.yaml + type: config|noreplace + file_info: + mode: 0640 + owner: root + group: mirumd + + - src: pkg/mirumw-default.yaml + dst: /etc/mirum/mirumw-default.yaml type: config|noreplace file_info: mode: 0640 owner: root - group: mirum + group: mirumw - src: pkg/mirumd.service dst: /usr/lib/systemd/system/mirumd.service file_info: mode: 0644 + - src: pkg/mirumw@.service + dst: /usr/lib/systemd/system/mirumw@.service + file_info: + mode: 0644 + - dst: /var/lib/mirumd type: dir file_info: - mode: 0755 - owner: root - group: mirum + mode: 0750 + owner: mirumd + group: mirumd + + - dst: /var/lib/mirumw + type: dir + file_info: + mode: 0750 + owner: mirumw + group: mirumw scripts: diff --git a/pkg/mirumd.service b/pkg/mirumd.service index 7a424d0..5b1b78c 100644 --- a/pkg/mirumd.service +++ b/pkg/mirumd.service @@ -9,14 +9,14 @@ Wants=time-sync.target [Service] Type=notify -User=mirum -Group=mirum +User=mirumd +Group=mirumd Restart=always RestartSec=30 WatchdogSec=30 NotifyAccess=main ExecPaths=/usr/local/bin/mirumd /usr/lib -ExecStart=/usr/local/bin/mirumd -config=/etc/mirumd.yaml +ExecStart=/usr/local/bin/mirumd --config=/etc/mirum/mirumd.yaml LimitCORE=infinity LimitNOFILE=500000 AmbientCapabilities=CAP_NET_BIND_SERVICE diff --git a/pkg/mirumw-default.yaml b/pkg/mirumw-default.yaml new file mode 100644 index 0000000..1fc4eee --- /dev/null +++ b/pkg/mirumw-default.yaml @@ -0,0 +1,4 @@ +# Copyright (c) 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +server: localhost:2026 diff --git a/pkg/mirumw@.service b/pkg/mirumw@.service new file mode 100644 index 0000000..cf7ddd5 --- /dev/null +++ b/pkg/mirumw@.service @@ -0,0 +1,48 @@ +# Copyright (c) 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +[Unit] +Description=Mirum worker %i (modern CI platform) +Requires=network-online.target +After=time-sync.target network-online.target remote-fs.target nss-lookup.target +Wants=time-sync.target + +[Service] +Type=notify +User=mirumw +Group=mirumw +Restart=always +RestartSec=30 +WatchdogSec=30 +NotifyAccess=main +ExecPaths=/usr/local/bin/mirumw +ExecStart=/usr/local/bin/mirumw --config=/etc/mirum/mirumw-%i.yaml +LimitCORE=infinity +LimitNOFILE=500000 +AmbientCapabilities= + +# %p is resolved to the systemd unit name +LogsDirectory=%p +StateDirectory=%p +CacheDirectory=%p +RuntimeDirectory=%p + +UMask=0077 +LockPersonality=yes +NoNewPrivileges=yes +PrivateTmp=true +ProtectClock=yes +ProtectControlGroups=yes +ProtectHome=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +ProtectSystem=strict +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictNamespaces=yes +RestrictSUIDSGID=yes + +[Install] +# service should not start from the rescue shell (rescue.target). +WantedBy=multi-user.target diff --git a/pkg/scripts/postinstall.sh b/pkg/scripts/postinstall.sh index 13b970c..2c80442 100644 --- a/pkg/scripts/postinstall.sh +++ b/pkg/scripts/postinstall.sh @@ -6,5 +6,8 @@ set -e if [ -x "/bin/systemctl" ] && [ -d /run/systemd/system ] && [ -f /usr/lib/systemd/system/mirumd.service ]; then /bin/systemctl daemon-reload - /bin/systemctl enable mirumd + + # Don't enable by default, don't know in advance whether it's a daemon or a worker + # /bin/systemctl enable mirumd + # /bin/systemctl enable mirumw fi diff --git a/pkg/scripts/preinstall.sh b/pkg/scripts/preinstall.sh index 6f7b91f..d15fee2 100644 --- a/pkg/scripts/preinstall.sh +++ b/pkg/scripts/preinstall.sh @@ -4,14 +4,11 @@ set -e -PROGRAM=mirum -MIRUM_USER=${MIRUM_USER:-mirum} -MIRUM_GROUP=${MIRUM_GROUP:-${MIRUM_USER}} - -if ! getent group $MIRUM_GROUP >/dev/null; then - groupadd --system $MIRUM_GROUP -fi - -if ! getent passwd $MIRUM_USER >/dev/null; then - useradd --system --gid $MIRUM_GROUP --no-create-home --shell /usr/sbin/nologin $MIRUM_USER -fi +for svc in mirumd mirumw; do + if ! getent group $svc >/dev/null; then + groupadd --system $svc + fi + if ! getent passwd $svc >/dev/null; then + useradd --system --gid $svc --no-create-home --shell /usr/sbin/nologin $svc + fi +done diff --git a/pkg/scripts/preremove.sh b/pkg/scripts/preremove.sh index c77f55b..fdb71a8 100644 --- a/pkg/scripts/preremove.sh +++ b/pkg/scripts/preremove.sh @@ -4,7 +4,10 @@ set -e -if [ -x "/bin/systemctl" ] && [ -d /run/systemd/system ] && [ -f /usr/lib/systemd/system/mirumd.service ]; then +if [ -x "/bin/systemctl" ] && [ -d /run/systemd/system ]; then /bin/systemctl stop mirumd.service || true /bin/systemctl disable mirumd.service || true + + /bin/systemctl stop 'mirumw@*' || true + /bin/systemctl disable mirumw@.service || true fi -- Gilti