diff --git a/.github/workflows/push-image.yaml b/.github/workflows/push-image.yaml index a9f90c7..73d83ce 100644 --- a/.github/workflows/push-image.yaml +++ b/.github/workflows/push-image.yaml @@ -3,6 +3,8 @@ name: Push Image on: push: # Sequence of patterns matched against refs/tags + branches: + - 'fix-multiarch-build' tags: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 @@ -10,7 +12,7 @@ on: workflow_dispatch: jobs: - x86_64: + multiarch: runs-on: ubuntu-latest permissions: contents: read @@ -18,28 +20,15 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v21 - - name: Build - run: | - nix build .#container - skopeo login --username "${{ github.actor }}" --password "${{ secrets.GITHUB_TOKEN }}" ghcr.io - skopeo copy docker-archive://$(readlink -f ./result) docker://ghcr.io/chrisrx/quake-kube:latest - - aarch64: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Checkout - uses: actions/checkout@v3 - - run: sudo apt-get install -y qemu-user-static - - uses: cachix/install-nix-action@v21 + - uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + - uses: DeterminateSystems/nix-installer-action@v9 with: - extra_nix_config: | - system = aarch64-linux - - name: Build - run: | - nix build .#container - skopeo login --username "${{ github.actor }}" --password "${{ secrets.GITHUB_TOKEN }}" ghcr.io - skopeo copy docker-archive://$(readlink -f ./result) docker://ghcr.io/chrisrx/quake-kube:latest + extra-conf: | + extra-platforms = aarch64-linux + - uses: DeterminateSystems/magic-nix-cache-action@v2 + - run: nix run --impure .#dockerManifest + env: + VERSION: "latest" + GITHUB_TOKEN: ${{ github.token }} diff --git a/cmd/q3/app/run/run.go b/cmd/q3/app/run/run.go index f5ab07a..14aa001 100644 --- a/cmd/q3/app/run/run.go +++ b/cmd/q3/app/run/run.go @@ -68,9 +68,6 @@ func NewCommand() *cobra.Command { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - sctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - defer stop() - qs := quakeserver.Server{ Addr: opts.ServerAddr, ConfigFile: opts.ConfigFile, @@ -79,9 +76,24 @@ func NewCommand() *cobra.Command { ShutdownDelay: opts.ShutdownDelay, } go func() { + // The main context should only cancel after the quake server is + // finished. This allows for graceful termination and the child process + // to be safely killed before exiting. defer cancel() - if err := qs.Start(sctx); err != nil { + ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + defer stop() + + registerSecondInterrupt(ctx.Done(), func() { + fmt.Println("\rCTRL+C pressed again, shutting down ...") + + // We still need to call HardStop to ensure that the underlying child + // process is killed before exiting. + qs.HardStop() + os.Exit(1) + }) + + if err := qs.Start(ctx); err != nil { log.Printf("quakeserver: %v\n", err) } }() @@ -113,3 +125,14 @@ func NewCommand() *cobra.Command { cmd.Flags().StringVar(&opts.SeedContentURL, "seed-content-url", "", "seed content from another content server") return cmd } + +func registerSecondInterrupt(ready <-chan struct{}, fn func()) { + go func() { + <-ready + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGINT) + <-c + signal.Stop(c) + fn() + }() +} diff --git a/flake.lock b/flake.lock index 5f3a970..c75e43b 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,23 @@ { "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1701473968, + "narHash": "sha256-YcVE5emp1qQ8ieHUnxt1wCZCC3ZfAS+SRRWZ2TMda7E=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "34fed993f1674c8d06d58b37ce1e0fe5eebcb9f5", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -18,6 +36,29 @@ "type": "github" } }, + "flocken": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": [ + "nixpkgs" + ], + "systems": "systems_2" + }, + "locked": { + "lastModified": 1704105102, + "narHash": "sha256-c4VWO9plhINjQzYPHSKURWgQ2D2q24aI3OIN0MTPjz0=", + "owner": "mirkolenz", + "repo": "flocken", + "rev": "3a846dfca17f989805d9f4177de85c96dc0f8542", + "type": "github" + }, + "original": { + "owner": "mirkolenz", + "ref": "v2", + "repo": "flocken", + "type": "github" + } + }, "gomod2nix": { "inputs": { "flake-utils": [ @@ -57,9 +98,28 @@ "type": "github" } }, + "nixpkgs-lib": { + "locked": { + "dir": "lib", + "lastModified": 1701253981, + "narHash": "sha256-ztaDIyZ7HrTAfEEUt9AtTDNoCYxUdSd6NrRHaYOIxtk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e92039b55bcd58469325ded85d4f58dd5a4eaf58", + "type": "github" + }, + "original": { + "dir": "lib", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, "root": { "inputs": { "flake-utils": "flake-utils", + "flocken": "flocken", "gomod2nix": "gomod2nix", "nixpkgs": "nixpkgs" } @@ -78,6 +138,21 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index ebb6ada..a18e0e7 100644 --- a/flake.nix +++ b/flake.nix @@ -6,8 +6,10 @@ inputs.gomod2nix.url = "github:nix-community/gomod2nix"; inputs.gomod2nix.inputs.nixpkgs.follows = "nixpkgs"; inputs.gomod2nix.inputs.flake-utils.follows = "flake-utils"; + inputs.flocken.url = "github:mirkolenz/flocken/v2"; + inputs.flocken.inputs.nixpkgs.follows = "nixpkgs"; - outputs = { self, nixpkgs, flake-utils, gomod2nix }: + outputs = { self, nixpkgs, flake-utils, gomod2nix, flocken }: (flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" ] (system: let @@ -98,6 +100,15 @@ ]; config.Cmd = [ "${packages.default}/bin/q3" ]; }; + legacyPackages.dockerManifest = flocken.legacyPackages.${system}.mkDockerManifest { + github = { + enable = true; + repo = "chrisrx/quake-kube"; + token = builtins.getEnv "GITHUB_TOKEN"; + }; + version = builtins.getEnv "VERSION"; + images = with self.packages; [ x86_64-linux.container aarch64-linux.container ]; + }; devShells.default = pkgs.mkShell { diff --git a/internal/quake/server/config.go b/internal/quake/server/config.go index a4b5a1f..6193094 100644 --- a/internal/quake/server/config.go +++ b/internal/quake/server/config.go @@ -123,6 +123,7 @@ type ServerConfig struct { Hostname string `name:"sv_hostname"` MaxClients int `name:"sv_maxclients"` Password string `name:"rconpassword"` + ListServer string `name:"sv_master1"` } func (c *Config) Marshal() ([]byte, error) { diff --git a/internal/quake/server/config_test.go b/internal/quake/server/config_test.go index fe21346..aa45903 100644 --- a/internal/quake/server/config_test.go +++ b/internal/quake/server/config_test.go @@ -63,6 +63,7 @@ seta sv_allowDownload "0" seta sv_hostname "quakekube" seta sv_maxclients "12" seta rconpassword "changeme" +seta sv_master1 "" set d0 "seta g_gametype 0 ; map q3dm7 ; set nextmap vstr d1" set d1 "seta g_gametype 0 ; map q3dm17 ; set nextmap vstr d2" set d2 "seta g_gametype 4 ; capturelimit 8 ; map q3wctf1 ; set nextmap vstr d3" diff --git a/internal/quake/server/gamefiles.go b/internal/quake/server/gamefiles.go index 8d09b90..312c3a3 100644 --- a/internal/quake/server/gamefiles.go +++ b/internal/quake/server/gamefiles.go @@ -8,6 +8,9 @@ import ( "log" "os" "path/filepath" + "strings" + + contentutil "github.com/ChrisRx/quake-kube/internal/quake/content/util" ) //go:embed EULA.txt @@ -44,6 +47,11 @@ func ExtractGameFiles(dir string) error { if err := os.WriteFile(path, data, 0644); err != nil { return err } + if strings.HasPrefix(hdr.Name, "linuxq3ademo") || strings.HasPrefix(hdr.Name, "linuxq3apoint") { + if err := contentutil.ExtractGzip(path, dir); err != nil { + return err + } + } } } return nil diff --git a/internal/quake/server/gamefiles.tar b/internal/quake/server/gamefiles.tar index 58b13c5..a702db8 100644 Binary files a/internal/quake/server/gamefiles.tar and b/internal/quake/server/gamefiles.tar differ diff --git a/internal/quake/server/server.go b/internal/quake/server/server.go index edb90f3..9fc8f19 100644 --- a/internal/quake/server/server.go +++ b/internal/quake/server/server.go @@ -48,6 +48,8 @@ type Server struct { Dir string WatchInterval time.Duration ShutdownDelay time.Duration + + cmd *exec.Cmd } func (s *Server) Start(ctx context.Context) error { @@ -72,10 +74,10 @@ func (s *Server) Start(ctx context.Context) error { "+set", "com_gamename", "Quake3Arena", "+exec", "server.cfg", } - cmd := exec.CommandContext(context.Background(), "ioq3ded", args...) - cmd.Dir = s.Dir - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + s.cmd = exec.CommandContext(context.Background(), "ioq3ded", args...) + s.cmd.Dir = s.Dir + s.cmd.Stdout = os.Stdout + s.cmd.Stderr = os.Stderr if s.ConfigFile == "" { cfg := Default() @@ -86,21 +88,21 @@ func (s *Server) Start(ctx context.Context) error { if err := os.WriteFile(filepath.Join(s.Dir, "baseq3/server.cfg"), data, 0644); err != nil { return err } - if err := cmd.Start(); err != nil { + if err := s.cmd.Start(); err != nil { return err } - return cmd.Wait() + return s.cmd.Wait() } if err := s.reload(); err != nil { return err } - if err := cmd.Start(); err != nil { + if err := s.cmd.Start(); err != nil { return err } go func() { - if err := cmd.Wait(); err != nil { + if err := s.cmd.Wait(); err != nil { log.Println(err) } }() @@ -140,8 +142,8 @@ func (s *Server) Start(ctx context.Context) error { } defer func() { - if cmd.Process != nil { - if err := cmd.Process.Kill(); err != nil { + if s.cmd.Process != nil { + if err := s.cmd.Process.Kill(); err != nil { log.Printf("couldn't kill process: %v\n", err) } } @@ -154,11 +156,11 @@ func (s *Server) Start(ctx context.Context) error { return err } configReloads.Inc() - if err := cmd.Restart(ctx); err != nil { + if err := s.cmd.Restart(ctx); err != nil { return err } go func() { - if err := cmd.Wait(); err != nil { + if err := s.cmd.Wait(); err != nil { log.Println(err) } }() @@ -221,6 +223,14 @@ func (s *Server) GracefulStop() { } } +func (s *Server) HardStop() { + if s.cmd.Process != nil { + if err := s.cmd.Process.Kill(); err != nil { + log.Printf("couldn't kill process: %v\n", err) + } + } +} + func (s *Server) reload() error { cfg, err := ReadConfigFromFile(s.ConfigFile) if err != nil { diff --git a/internal/run/run.go b/internal/run/run.go index 921c430..44c95a3 100644 --- a/internal/run/run.go +++ b/internal/run/run.go @@ -6,29 +6,29 @@ import ( "time" ) -func Until(do func(), stop <-chan struct{}, interval time.Duration) { +func Until(fn func(), stop <-chan struct{}, interval time.Duration) { if err := runUntil(func() error { - do() + fn() return nil }, stop, interval); err != nil { log.Println(err) } } -func UntilE(do func() error, stop <-chan struct{}, interval time.Duration) error { - return runUntil(do, stop, interval) +func UntilE(fn func() error, stop <-chan struct{}, interval time.Duration) error { + return runUntil(fn, stop, interval) } var ErrStopped = errors.New("stopped") -func runUntil(do func() error, stop <-chan struct{}, interval time.Duration) error { +func runUntil(fn func() error, stop <-chan struct{}, interval time.Duration) error { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ticker.C: - if err := do(); err != nil { + if err := fn(); err != nil { return err } case <-stop: