Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.13.2
version: v2.14.0
args: --timeout=10m

- name: Unit Tests
Expand Down
20 changes: 10 additions & 10 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/gameap/daemon

go 1.27.0
go 1.27.2

require (
github.com/bodgit/sevenzip v1.6.5
Expand Down Expand Up @@ -79,7 +79,7 @@ require (
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/felixge/httpsnoop v1.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/google/s2a-go v0.1.9 // indirect
Expand Down Expand Up @@ -112,17 +112,17 @@ require (
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/otel v1.45.0 // indirect
go.opentelemetry.io/otel/metric v1.45.0 // indirect
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
go.opentelemetry.io/otel/trace v1.45.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/crypto v0.56.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/net v0.60.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/text v0.42.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/api v0.294.0 // indirect
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d // indirect
Expand Down
40 changes: 20 additions & 20 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ github.com/gameap/gameapctl v0.34.0/go.mod h1:GEUPbmHDrnFYxVUqN3FpE7afnrEQkjtdrl
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
Expand Down Expand Up @@ -241,30 +241,30 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns=
go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI=
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
Expand All @@ -273,8 +273,8 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
21 changes: 20 additions & 1 deletion internal/app/domain/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ const autostartSettingKey = "autostart"
const autostartCurrentSettingKey = "autostart_current"
const updateBeforeStartSettingKey = "update_before_start"

// ErrServerBlocked is returned for an attempt to start a server suspended in
// the panel. The text matches the panel's own refusal.
var ErrServerBlocked = errors.New("server is blocked")

type workDirReader interface {
WorkDir() string
}
Expand Down Expand Up @@ -272,6 +276,21 @@ func (s *Server) Blocked() bool {
return s.blocked
}

// IsSuspended reports whether the panel suspended the server: it must not be
// started by any path, and a copy still running is to be stopped. Every such
// check goes through here, so a future suspension reason (an expiry date) only
// has to be added in one place.
func (s *Server) IsSuspended() bool {
s.mu.RLock()
defer s.mu.RUnlock()

return s.isSuspended()
}

func (s *Server) isSuspended() bool {
return s.blocked
}

func (s *Server) UUID() string {
s.mu.RLock()
defer s.mu.RUnlock()
Expand Down Expand Up @@ -660,7 +679,7 @@ func (s *Server) CanAutoStart() bool {
s.mu.RLock()
defer s.mu.RUnlock()

if s.blocked {
if s.isSuspended() {
return false
}

Expand Down
23 changes: 22 additions & 1 deletion internal/app/domain/server_autostart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,11 +296,32 @@ func TestServer_Set_ConvergesAfterTheDaemonActsAgain(t *testing.T) {
assert.True(t, server.AutoStart())
}

// A suspension arrives the same way as any other change: the panel pushes the
// whole server, and lifting it is just another push.
func TestServer_IsSuspended_FollowsThePanel(t *testing.T) {
server := newTestServerForAutostart(true, false, Settings{autostartSettingKey: "1"})
require.False(t, server.IsSuspended())

pushServer(server, true, Settings{autostartSettingKey: "1"})

assert.True(t, server.IsSuspended())
assert.False(t, server.CanAutoStart(), "a suspended server is never started by the loop")

pushServer(server, false, Settings{autostartSettingKey: "1"})

assert.False(t, server.IsSuspended())
assert.True(t, server.CanAutoStart())
}

func pushServerSettings(server *Server, settings Settings) {
pushServer(server, false, settings)
}

func pushServer(server *Server, blocked bool, settings Settings) {
server.Set(
true,
ServerInstalled,
false,
blocked,
"test",
"759b875e-d910-11eb-aff7-d796d7fcf7ef",
"759b875e",
Expand Down
8 changes: 8 additions & 0 deletions internal/app/game_server_commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ const (
ErrorResult = int(domain.ErrorResult)
)

// Written to the command output of a suspended server, so the task log in the
// panel says in plain words why the server stayed down.
const (
suspendedServerStartRefused = "The server is suspended in the panel " +
"and cannot be started until the suspension is lifted.\n"
suspendedServerNotStartedAgain = "The server is suspended in the panel and is not started again.\n"
)

type LoadServerCommandFunc func(cmd domain.ServerCommand, server *domain.Server) contracts.GameServerCommand

var nilLoadServerCommandFunc = func(_ domain.ServerCommand, _ *domain.Server) contracts.GameServerCommand {
Expand Down
9 changes: 9 additions & 0 deletions internal/app/game_server_commands/install_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,15 @@ func (cmd *installServer) startServerIfNeeded(ctx context.Context, server *domai
return nil
}

// The start command would refuse anyway, but as an error, and a scheduled
// update that did everything it was asked to would be reported as failed.
if server.IsSuspended() {
logger.Info(ctx, "Game server is suspended, it is not started again after installation/updating")
_, _ = cmd.installOutput.Write([]byte(suspendedServerNotStartedAgain))

return nil
}

err := cmd.startCommand.Execute(ctx, server)
if err != nil {
return errors.WithMessage(
Expand Down
79 changes: 77 additions & 2 deletions internal/app/game_server_commands/install_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,59 @@ func TestInstallation_ServerInstalledFromLocalRepository(t *testing.T) {
assert.FileExists(t, workPath+"/test-server/directory_file.txt")
}

// An update of a running server stops it and starts it again afterwards. A
// suspended server stays stopped, and the update itself still succeeds: it is a
// scheduled update that did what it was asked, not a failed one.
func TestInstallation_ServerRunningBeforeInstallation(t *testing.T) {
tests := []struct {
name string
server func(t *testing.T) *domain.Server
wantActive bool
wantOutput bool
}{
{
name: "is started again",
server: givenLocalInstallationServer,
wantActive: true,
wantOutput: false,
},
{
name: "is not started again while suspended",
server: givenSuspendedLocalInstallationServer,
wantActive: false,
wantOutput: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &config.Config{
WorkPath: t.TempDir(),
}
install := newInstallServer(
cfg,
components.NewExecutor(),
processmanager.NewSimple(cfg, components.NewExecutor(), components.NewExecutor()),
mocks.NewServerRepository(),
commandmocks.LoadServerCommand(domain.Status),
commandmocks.LoadServerCommand(domain.Stop),
commandmocks.LoadServerCommand(domain.Start),
)
server := tt.server(t)

err := install.Execute(context.Background(), server)

require.NoError(t, err)
assert.Equal(t, tt.wantActive, server.IsActive())
assert.Equal(
t,
tt.wantOutput,
bytes.Contains(install.ReadOutput(), []byte(suspendedServerNotStartedAgain)),
)
})
}
}

func TestInstallation_RunAfterInstallScript(t *testing.T) {
workPath, err := os.MkdirTemp(os.TempDir(), "gameap-daemon-test")
defer func(path string) {
Expand Down Expand Up @@ -487,6 +540,22 @@ func givenRemoteInstallationServer(t *testing.T) *domain.Server {
func givenLocalInstallationServer(t *testing.T) *domain.Server {
t.Helper()

game, gameMod := givenLocalInstallationGame(t)

return givenServer(t, game, gameMod)
}

func givenSuspendedLocalInstallationServer(t *testing.T) *domain.Server {
t.Helper()

game, gameMod := givenLocalInstallationGame(t)

return givenServerWithState(t, game, gameMod, true)
}

func givenLocalInstallationGame(t *testing.T) (domain.Game, domain.GameMod) {
t.Helper()

pathToFileZip, err := filepath.Abs("../../../test/files/file.zip")
if err != nil {
t.Fatal(err)
Expand All @@ -509,7 +578,7 @@ func givenLocalInstallationServer(t *testing.T) *domain.Server {
RemoteRepository: "https://files.gameap.ru/mod-game.tar.gz",
}

return givenServer(t, game, gameMod)
return game, gameMod
}

func givenLocalInstallationServerWithAfterInstallScript(t *testing.T) *domain.Server {
Expand Down Expand Up @@ -538,11 +607,17 @@ func givenLocalInstallationServerWithAfterInstallScript(t *testing.T) *domain.Se
func givenServer(t *testing.T, game domain.Game, gameMod domain.GameMod) *domain.Server {
t.Helper()

return givenServerWithState(t, game, gameMod, false)
}

func givenServerWithState(t *testing.T, game domain.Game, gameMod domain.GameMod, blocked bool) *domain.Server {
t.Helper()

return domain.NewServer(
1,
true,
domain.ServerInstalled,
false,
blocked,
"name",
"759b875e-d910-11eb-aff7-d796d7fcf7ef",
"759b875e",
Expand Down
31 changes: 17 additions & 14 deletions internal/app/game_server_commands/restart_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,16 @@ func newDefaultRestartServer(
}

func (cmd *defaultRestartServer) Execute(ctx context.Context, server *domain.Server) error {
cmd.output = components.NewSafeBuffer()
// Refused as a whole rather than left to the start command: the default
// restart script restarts through the process manager and never runs it, and
// the stop/start path would stop the server before the start is refused.
if server.IsSuspended() {
_, _ = cmd.output.Write([]byte(suspendedServerStartRefused))
cmd.SetResult(ErrorResult)
cmd.SetComplete()

return errors.WithMessage(domain.ErrServerBlocked, "[game_server_commands.defaultRestartServer] restart refused")
}

// Checked before either restart path runs. The stop/start path would otherwise stop a healthy
// server first and only then fail in the start command, leaving a server that was running
Expand Down Expand Up @@ -106,21 +115,15 @@ func (cmd *defaultRestartServer) restartViaStopStart(ctx context.Context, server
}

func (cmd *defaultRestartServer) ReadOutput() []byte {
var err error
var out []byte
out, err := io.ReadAll(cmd.output)
if err != nil {
return []byte(err.Error())
}

if cmd.cfg.Scripts.Restart == "" {
statusOut := cmd.statusServer.ReadOutput()
stopOut := cmd.stopServer.ReadOutput()
startOut := cmd.startServer.ReadOutput()
out = append(out, statusOut...)
out = append(out, stopOut...)
out = append(out, startOut...)
} else {
out, err = io.ReadAll(cmd.output)
if err != nil {
return []byte(err.Error())
}
out = append(out, cmd.statusServer.ReadOutput()...)
out = append(out, cmd.stopServer.ReadOutput()...)
out = append(out, cmd.startServer.ReadOutput()...)
}

return out
Expand Down
Loading
Loading