Skip to content

Commit

Permalink
refactor: various
Browse files Browse the repository at this point in the history
  • Loading branch information
smsunarto committed May 17, 2024
1 parent 03aa9eb commit a287716
Show file tree
Hide file tree
Showing 9 changed files with 378 additions and 275 deletions.
439 changes: 203 additions & 236 deletions cmd/world/cardinal/dev.go

Large diffs are not rendered by default.

65 changes: 33 additions & 32 deletions cmd/world/cardinal/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import (
"fmt"
"strings"

"github.com/charmbracelet/lipgloss"
"github.com/rotisserie/eris"
"github.com/rs/zerolog"
"github.com/spf13/cobra"

"golang.org/x/sync/errgroup"

Check failure on line 10 in cmd/world/cardinal/start.go

View workflow job for this annotation

GitHub Actions / Go

File is not `gci`-ed with --skip-generated -s standard -s default -s prefix(pkg.world.dev/world-cli) -s blank -s dot --custom-order (gci)

Check failure on line 10 in cmd/world/cardinal/start.go

View workflow job for this annotation

GitHub Actions / Go

File is not `gci`-ed with --skip-generated -s standard -s default -s prefix(pkg.world.dev/world-cli) -s blank -s dot --custom-order (gci)
"pkg.world.dev/world-cli/common"
"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/common/logger"
"pkg.world.dev/world-cli/common/teacmd"
Expand Down Expand Up @@ -41,6 +41,8 @@ var (
zerolog.Disabled.String(),
zerolog.TraceLevel.String(),
}, ", ")

ErrGracefulExit = eris.New("Process gracefully exited")
)

// startCmd starts your Cardinal game shard stack
Expand Down Expand Up @@ -100,42 +102,41 @@ This will start the following Docker services and its dependencies:
}

runEditor, err := cmd.Flags().GetBool(flagEditor)
if err == nil && runEditor {
// Find an unused port for the Cardinal Editor
cardinalEditorPort, findPortError := findUnusedPort(cePortStart, cePortEnd)
if findPortError == nil {
cmdBlue := lipgloss.NewStyle().Foreground(lipgloss.Color("6"))
fmt.Println(cmdBlue.Render("Preparing Cardinal Editor..."))
fmt.Println(cmdBlue.Render(fmt.Sprint("Cardinal Editor will be run on localhost:", cardinalEditorPort)))
cePrepChan := make(chan struct{})
go func() {
err := runCardinalEditor(cfg.RootDir, cfg.GameDir, cardinalEditorPort, cePrepChan)
if err != nil {
cmdStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("11"))
fmt.Println(cmdStyle.Render("Warning: Failed to run Cardinal Editor"))
logger.Error(eris.Wrap(err, "Failed to run Cardinal Editor"))

// continue if error
cePrepChan <- struct{}{}
}
}()
// Waiting cardinal editor preparation
<-cePrepChan
} else {
// just log the error if the editor fails to run
logger.Error(eris.Wrap(findPortError, "Failed to find an unused port for Cardinal Editor"))
}
} else {
// just log the error if the editor fails to run
logger.Error(eris.Wrap(err, "Failed to run Cardinal Editor"))
if err != nil {
return err
}

fmt.Println("Starting Cardinal game shard...")
fmt.Println("This may take a few minutes to rebuild the Docker images.")
fmt.Println("Use `world cardinal dev` to run Cardinal faster/easier in development mode.")

err = teacmd.DockerStartAll(cfg)
if err != nil {
group, ctx := errgroup.WithContext(cmd.Context())

// Start the World Engine stack
group.Go(func() error {
if err := teacmd.DockerStartAll(cfg); err != nil {
return eris.Wrap(err, "Encountered an error with Docker")
}
return eris.Wrap(ErrGracefulExit, "Stack terminated")
})

// Start Cardinal Editor is flag is set to true
if runEditor {
editorPort, err := common.FindUnusedPort(cePortStart, cePortEnd)
if err != nil {
return eris.Wrap(err, "Failed to find an unused port for Cardinal Editor")
}

group.Go(func() error {
if err := startCardinalEditor(ctx, cfg.RootDir, cfg.GameDir, editorPort); err != nil {
return eris.Wrap(err, "Encountered an error with Cardinal Editor")
}
return eris.Wrap(ErrGracefulExit, "Cardinal Editor terminated")
})
}

// If any of the group's goroutines is terminated non-gracefully, we want to treat it as an error.
if err := group.Wait(); err != nil && !eris.Is(err, ErrGracefulExit) {
return err
}

Expand Down
52 changes: 52 additions & 0 deletions cmd/world/cardinal/util_editor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package cardinal

import (
"context"
"fmt"
"net/http"
"path/filepath"
"time"

"github.com/rotisserie/eris"
"golang.org/x/sync/errgroup"

"pkg.world.dev/world-cli/common/teacmd"
)

const ceReadTimeout = 5 * time.Second

// startCardinalEditor runs the Cardinal Editor
func startCardinalEditor(ctx context.Context, rootDir string, gameDir string, port int) error {
if err := teacmd.SetupCardinalEditor(rootDir, gameDir); err != nil {
return err
}

// Create a new HTTP server
fs := http.FileServer(http.Dir(filepath.Join(rootDir, teacmd.TargetEditorDir)))
http.Handle("/", fs)
server := &http.Server{
Addr: fmt.Sprintf(":%d", port),
ReadTimeout: ceReadTimeout,
}

group, ctx := errgroup.WithContext(ctx)
group.Go(func() error {
if err := server.ListenAndServe(); err != nil && !eris.Is(err, http.ErrServerClosed) {
return eris.Wrap(err, "Cardinal Editor server encountered an error")
}
return nil
})
group.Go(func() error {
<-ctx.Done()
if err := server.Shutdown(ctx); err != nil {
return eris.Wrap(err, "Failed to gracefully shutdown server")
}
return nil
})

if err := group.Wait(); err != nil {
return err
}

return nil
}
31 changes: 30 additions & 1 deletion cmd/world/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import (
"io"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"github.com/charmbracelet/lipgloss"
"github.com/hashicorp/go-version"
"github.com/rotisserie/eris"
"github.com/spf13/cobra"

Check failure on line 18 in cmd/world/root/root.go

View workflow job for this annotation

GitHub Actions / Go

File is not `gci`-ed with --skip-generated -s standard -s default -s prefix(pkg.world.dev/world-cli) -s blank -s dot --custom-order (gci)

Check failure on line 18 in cmd/world/root/root.go

View workflow job for this annotation

GitHub Actions / Go

File is not `gci`-ed with --skip-generated -s standard -s default -s prefix(pkg.world.dev/world-cli) -s blank -s dot --custom-order (gci)

"pkg.world.dev/world-cli/cmd/world/cardinal"
"pkg.world.dev/world-cli/cmd/world/evm"
"pkg.world.dev/world-cli/common/config"
Expand Down Expand Up @@ -51,6 +52,12 @@ func init() {
// Enable case-insensitive commands
cobra.EnableCaseInsensitive = true

// Disable printing usage help text when command returns a non-nil error
rootCmd.SilenceUsage = true

// Injects a context that is canceled when a sigterm signal is received
rootCmd.SetContext(contextWithSigterm(context.Background()))

// Register groups
rootCmd.AddGroup(&cobra.Group{ID: "starter", Title: "Getting Started:"})
rootCmd.AddGroup(&cobra.Group{ID: "core", Title: "Tools:"})
Expand Down Expand Up @@ -139,3 +146,25 @@ func Execute() {
// print log stack
logger.PrintLogs()
}

// contextWithSigterm provides a context that automatically terminates when either the parent context is canceled or
// when a termination signal is received
func contextWithSigterm(ctx context.Context) context.Context {
ctx, cancel := context.WithCancel(ctx)
textStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("3"))

go func() {
defer cancel()

signalCh := make(chan os.Signal, 1)
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)

select {
case <-signalCh:
fmt.Println(textStyle.Render("Interrupt signal received. Terminating..."))
case <-ctx.Done():
fmt.Println(textStyle.Render("Cancellation signal received. Terminating..."))
}
}()
return ctx
}
8 changes: 4 additions & 4 deletions cmd/world/root/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package root

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
Expand All @@ -13,8 +14,6 @@ import (

"github.com/spf13/cobra"
"gotest.tools/v3/assert"

"pkg.world.dev/world-cli/cmd/world/cardinal"
)

type healthResponse struct {
Expand Down Expand Up @@ -214,9 +213,10 @@ func TestDev(t *testing.T) {
assert.NilError(t, err)

// Start cardinal dev
ctx, cancel := context.WithCancel(context.Background())
rootCmd.SetArgs([]string{"cardinal", "dev"})
go func() {
err := rootCmd.Execute()
err := rootCmd.ExecuteContext(ctx)
assert.NilError(t, err)
}()

Expand All @@ -238,7 +238,7 @@ func TestDev(t *testing.T) {
assert.Assert(t, isGameLoopRunning)

// Shutdown the program
cardinal.StopChan <- struct{}{}
cancel()

// Check cardinal health (expected error), trying for 10 times
count := 0
Expand Down
17 changes: 17 additions & 0 deletions common/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package common

import (
"os"

"github.com/rotisserie/eris"
)

// WithEnv sets the environment variables from the given map.
func WithEnv(env map[string]string) error {
for key, value := range env {
if err := os.Setenv(key, value); err != nil {
return eris.Wrap(err, "Failed to set env var")
}
}
return nil
}
21 changes: 21 additions & 0 deletions common/port.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package common

import (
"fmt"
"net"
)

// FindUnusedPort finds an unused port in the range [start, end] for Cardinal Editor
func FindUnusedPort(start int, end int) (int, error) {
for port := start; port <= end; port++ {
address := fmt.Sprintf(":%d", port)
listener, err := net.Listen("tcp", address)
if err == nil {
if err := listener.Close(); err != nil {
return 0, err
}
return port, nil
}
}
return 0, fmt.Errorf("no available port in the range %d-%d", start, end)
}
18 changes: 17 additions & 1 deletion common/teacmd/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"path"
"slices"
"strings"

"github.com/magefile/mage/sh"
Expand Down Expand Up @@ -46,8 +47,23 @@ func dockerComposeWithCfg(cfg *config.Config, args ...string) error {
}

cmd.Env = env

if err := cmd.Run(); err != nil {
var exitCode *exec.ExitError
if errors.As(err, &exitCode) {
// Ignore exit codes 130, 137, and 143 as they are expected to be returned on termination.
// Exit code 130: Container terminated by Ctrl+C
// Exit code 137: Container terminated by SIGKILL
// Exit code 143: Container terminated by SIGTERM
expectedExitCodes := []int{130, 137, 143}
if slices.Contains(expectedExitCodes, exitCode.ExitCode()) {
return nil
}
return err
}
}

return cmd.Run()
// return sh.RunWith(cfg.DockerEnv, "docker", args...)
}

// DockerStart starts a given docker container by name.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ require (
github.com/muesli/termenv v0.15.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sync v0.1.0
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
Expand Down

0 comments on commit a287716

Please sign in to comment.