├── NOTICE ├── tst ├── mockito-extensions │ └── org.mockito.plugins.MockMaker ├── resources │ ├── child_process.sh │ ├── test_log │ └── parent_process.sh └── com │ └── amazon │ └── gamelift │ └── agent │ ├── utils │ ├── ExecutorServiceSafeRunnableTest.java │ ├── EcsMetadataReaderTest.java │ ├── RetryHelperTest.java │ └── AmazonGameLiftRetryConditionTest.java │ ├── model │ ├── ProcessTerminationReasonTest.java │ ├── OperatingSystemFamilyTest.java │ ├── websocket │ │ └── RefreshConnectionMessageTest.java │ └── RuntimeConfigurationTest.java │ ├── websocket │ ├── SdkWebsocketEndpointProviderTest.java │ ├── handlers │ │ ├── RefreshConnectionHandlerTest.java │ │ ├── ForceExitProcessHandlerTest.java │ │ └── NotifyGameSessionActivatedHandlerTest.java │ └── WebSocketConnectionProviderTest.java │ ├── command │ └── LinuxCommandTransformTest.java │ ├── logging │ ├── AgentLogFileFilterTest.java │ └── LogFileHelperTest.java │ ├── process │ ├── destroyer │ │ ├── ProcessDestroyerFactoryTest.java │ │ └── WindowsProcessDestroyerTest.java │ └── builder │ │ ├── ProcessBuilderFactoryTest.java │ │ └── LinuxProcessBuilderTest.java │ ├── cache │ ├── RuntimeConfigurationCacheLoaderTest.java │ └── ComputeAuthTokenCacheLoaderTest.java │ ├── ApplicationTest.java │ ├── AgentTest.java │ └── manager │ ├── DynamicRuntimeConfigurationManagerTest.java │ └── ComputeAuthTokenManagerTest.java ├── CODE_OF_CONDUCT.md ├── src ├── com │ └── amazon │ │ └── gamelift │ │ └── agent │ │ ├── utils │ │ ├── SystemEnvironmentProvider.java │ │ ├── RealSystemEnvironmentProvider.java │ │ ├── ExecutorServiceSafeRunnable.java │ │ ├── AmazonGameLiftRetryCondition.java │ │ └── EnvironmentHelper.java │ │ ├── model │ │ ├── EventDedupe.java │ │ ├── constants │ │ │ ├── ProcessConstants.java │ │ │ ├── WebSocketActions.java │ │ │ ├── LogCredentials.java │ │ │ ├── EnvironmentConstants.java │ │ │ └── GameLiftCredentials.java │ │ ├── ProcessStatus.java │ │ ├── exception │ │ │ ├── ConflictException.java │ │ │ ├── MalformedRequestException.java │ │ │ ├── NotFoundException.java │ │ │ ├── NotFinishedException.java │ │ │ ├── UnauthorizedException.java │ │ │ ├── InvalidRequestException.java │ │ │ ├── BadExecutablePathException.java │ │ │ ├── NotReadyException.java │ │ │ ├── ThrottlingException.java │ │ │ ├── InternalServiceException.java │ │ │ └── AgentException.java │ │ ├── websocket │ │ │ ├── base │ │ │ │ ├── WebsocketMessage.java │ │ │ │ ├── ErrorWebsocketResponse.java │ │ │ │ ├── WebsocketRequest.java │ │ │ │ └── WebsocketResponse.java │ │ │ ├── ForceExitProcessMessage.java │ │ │ ├── DescribeRuntimeConfigurationRequest.java │ │ │ ├── NotifyGameSessionActivatedMessage.java │ │ │ ├── NotifyProcessRegisteredMessage.java │ │ │ ├── ForceExitServerProcessMessage.java │ │ │ ├── StartComputeTerminationMessage.java │ │ │ ├── RefreshConnectionMessage.java │ │ │ ├── SendHeartbeatResponse.java │ │ │ ├── GetFleetRoleCredentialsRequest.java │ │ │ ├── DescribeRuntimeConfigurationResponse.java │ │ │ ├── NotifyServerProcessTerminationRequest.java │ │ │ ├── SendHeartbeatRequest.java │ │ │ └── GetFleetRoleCredentialsResponse.java │ │ ├── gamelift │ │ │ ├── GetComputeAuthTokenResponse.java │ │ │ └── RegisterComputeResponse.java │ │ ├── RuntimeConfiguration.java │ │ ├── FleetRoleCredentialsConfiguration.java │ │ ├── OperatingSystemFamily.java │ │ ├── GameProcessConfiguration.java │ │ ├── ComputeStatus.java │ │ ├── AgentArgs.java │ │ ├── ProcessTerminationReason.java │ │ └── ConfiguredLogPaths.java │ │ ├── manager │ │ ├── RuntimeConfigurationManager.java │ │ ├── StaticRuntimeConfigurationManager.java │ │ ├── DynamicRuntimeConfigurationManager.java │ │ ├── ComputeAuthTokenManager.java │ │ ├── InstanceTerminationMonitor.java │ │ └── FleetRoleCredentialsConfigurationManager.java │ │ ├── process │ │ ├── destroyer │ │ │ ├── ProcessDestroyer.java │ │ │ ├── ProcessDestroyerFactory.java │ │ │ ├── WindowsProcessDestroyer.java │ │ │ └── LinuxProcessDestroyer.java │ │ └── builder │ │ │ ├── ProcessBuilderWrapper.java │ │ │ ├── ProcessBuilderFactory.java │ │ │ ├── WindowsProcessHandle.java │ │ │ └── WindowsProcessCommons.java │ │ ├── cli │ │ └── HelpRequestedException.java │ │ ├── component │ │ ├── CliComponent.java │ │ └── GameLiftAgentComponent.java │ │ ├── command │ │ ├── CommandTransform.java │ │ └── LinuxCommandTransform.java │ │ ├── windows │ │ ├── MoreNetapi32.java │ │ ├── MoreLMAccess.java │ │ └── MoreAdvapi32.java │ │ ├── websocket │ │ ├── handlers │ │ │ ├── DefaultHandler.java │ │ │ ├── StartComputeTerminationHandler.java │ │ │ ├── MessageHandler.java │ │ │ ├── NotifyGameSessionActivatedHandler.java │ │ │ ├── RefreshConnectionHandler.java │ │ │ ├── ForceExitProcessHandler.java │ │ │ ├── NotifyProcessRegisteredHandler.java │ │ │ └── ForceExitServerProcessHandler.java │ │ ├── SdkWebsocketEndpointProvider.java │ │ └── WebSocketExceptionProvider.java │ │ ├── logging │ │ ├── AgentLogFileFilter.java │ │ ├── GameSessionLogPath.java │ │ ├── GameSessionLogsErrorReadMeFile.java │ │ ├── UploadGameSessionLogsCallableFactory.java │ │ └── LogFileHelper.java │ │ ├── module │ │ ├── CliModule.java │ │ └── ClientModule.java │ │ └── cache │ │ ├── ComputeAuthTokenCacheLoader.java │ │ ├── RuntimeConfigCacheLoader.java │ │ └── FleetRoleCredentialsCacheLoader.java └── log4j2.xml ├── .github ├── pull_request_template.md ├── issue_template.md └── workflows │ └── maven_compile_and_test.yml ├── CHANGELOG.md ├── install.bat ├── install.sh └── CONTRIBUTING.md /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /tst/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | # Enable mocking of final classes, for mocking ProcessBuilder: https://github.com/mockito/mockito/wiki/What%27s-new-in-Mockito-2#mock-the-unmockable-opt-in-mocking-of-final-classesmethods 2 | mock-maker-inline -------------------------------------------------------------------------------- /tst/resources/child_process.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # 4 | # This script does nothing except stay alive for 10 seconds 5 | echo "Child process created, sleeping..." 6 | sleep 10 7 | echo "Child process timed out!" -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/utils/SystemEnvironmentProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.utils; 5 | 6 | public interface SystemEnvironmentProvider { 7 | /** 8 | * Get environment variable 9 | * @return 10 | */ 11 | String getenv(String env); 12 | } 13 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/EventDedupe.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model; 5 | 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | 10 | @Getter 11 | @Setter 12 | @AllArgsConstructor 13 | public class EventDedupe { 14 | private Integer counter; 15 | } 16 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/constants/ProcessConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.constants; 5 | 6 | import javax.inject.Singleton; 7 | 8 | @Singleton 9 | public final class ProcessConstants { 10 | 11 | private ProcessConstants() { } 12 | 13 | public static final int INVALID_LAUNCH_PATH_PROCESS_EXIT_CODE = 2; 14 | } 15 | -------------------------------------------------------------------------------- /tst/resources/test_log: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 2 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/manager/RuntimeConfigurationManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.manager; 5 | 6 | import com.amazon.gamelift.agent.model.RuntimeConfiguration; 7 | 8 | public interface RuntimeConfigurationManager { 9 | /** 10 | * Get RuntimeConfiguration 11 | * @return 12 | */ 13 | RuntimeConfiguration getRuntimeConfiguration(); 14 | } 15 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/process/destroyer/ProcessDestroyer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.process.destroyer; 5 | 6 | public interface ProcessDestroyer { 7 | /** 8 | * Terminates an underlying process and all its child processes. 9 | * @param internalProcess 10 | * @return 11 | */ 12 | void destroyProcess(Process internalProcess); 13 | } 14 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/utils/RealSystemEnvironmentProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.utils; 5 | 6 | public class RealSystemEnvironmentProvider implements SystemEnvironmentProvider { 7 | 8 | /** 9 | * Get environment variable 10 | * @return 11 | */ 12 | @Override 13 | public String getenv(final String env) { 14 | return System.getenv(env); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/ProcessStatus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model; 5 | 6 | /** 7 | * An internal representation of the status of game processes 8 | */ 9 | public enum ProcessStatus { 10 | Initializing, // The Process has been started, but has not yet connected using the GameLift SDK 11 | Active // The GameLiftAgent has been notified that the Process has successfully connected with the SDK 12 | } 13 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/exception/ConflictException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.exception; 5 | 6 | /** 7 | * ConflictException 8 | */ 9 | public class ConflictException extends AgentException { 10 | 11 | /** 12 | * Creates ConflictException with message and throwable 13 | */ 14 | public ConflictException(final String message, final Throwable exception) { 15 | super(message, exception, false); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tst/resources/parent_process.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # 4 | # This script creates a child process and then sleeps for 10 seconds 5 | # Usage: sh parent_process.sh -p /full/path/to/child_process.sh 6 | while getopts p: flag 7 | do 8 | case "${flag}" in 9 | p) child_script_path=${OPTARG};; 10 | esac 11 | done 12 | 13 | echo "Parent process created, starting child process." 14 | sh "$(child_script_path)" & 15 | echo "Parent process sleeping..." 16 | sleep 10 17 | echo "Parent process timed out!" -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/cli/HelpRequestedException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.cli; 5 | 6 | /** 7 | * Exception to indicate that the user has requested the help cli option and no other args were evaluated. 8 | */ 9 | public class HelpRequestedException extends RuntimeException { 10 | /** 11 | * Constructor for HelpRequestedException 12 | * @param message 13 | */ 14 | public HelpRequestedException(final String message) { 15 | super(message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/exception/MalformedRequestException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.exception; 5 | 6 | /** 7 | * MalformedRequestException 8 | */ 9 | public class MalformedRequestException extends AgentException { 10 | 11 | /** 12 | * Creates MalformedRequestException with message and throwable 13 | */ 14 | public MalformedRequestException(final String message, final Throwable exception) { 15 | super(message, exception, false); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/component/CliComponent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.component; 5 | 6 | import com.amazon.gamelift.agent.cli.AgentCliParser; 7 | import com.amazon.gamelift.agent.module.CliModule; 8 | import dagger.Component; 9 | 10 | /** 11 | * Interface for CLI dagger dependency injection. 12 | */ 13 | @Component(modules = CliModule.class) 14 | public interface CliComponent { 15 | /** 16 | * Builds a CLI Parser 17 | * @return 18 | */ 19 | AgentCliParser buildCliParser(); 20 | } 21 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/constants/WebSocketActions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.constants; 5 | 6 | public enum WebSocketActions { 7 | Default, 8 | DescribeRuntimeConfiguration, 9 | // @deprecated 10 | ForceExitProcess, 11 | ForceExitServerProcess, 12 | GetFleetRoleCredentials, 13 | NotifyGameSessionActivated, 14 | NotifyProcessRegistered, 15 | NotifyServerProcessTermination, 16 | RefreshConnection, 17 | SendHeartbeat, 18 | StartComputeTermination 19 | } 20 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/base/WebsocketMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket.base; 5 | 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.Getter; 9 | import lombok.NonNull; 10 | import lombok.Setter; 11 | import lombok.ToString; 12 | 13 | @Setter 14 | @Getter 15 | @ToString 16 | @EqualsAndHashCode 17 | public abstract class WebsocketMessage { 18 | @NonNull 19 | @JsonProperty(value = "Action") 20 | private String action; 21 | } 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Notes: 2 | 3 | ## Description 4 | A few sentences describing the overall goals of the pull request's commits. 5 | 6 | Please verify the following 7 | - [ ] Your PR is up-to-date with the main branch 8 | - [ ] There are new or updated tests validating the change 9 | - [ ] You have successfully run mvn build locally 10 | - [ ] Changelog updated 11 | 12 | ## Steps to test or reproduce 13 | Outline the steps to test or reproduce the PR here. 14 | 15 | ## License 16 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 17 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/manager/StaticRuntimeConfigurationManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.manager; 5 | 6 | import com.amazon.gamelift.agent.model.RuntimeConfiguration; 7 | 8 | import lombok.RequiredArgsConstructor; 9 | 10 | @RequiredArgsConstructor 11 | public class StaticRuntimeConfigurationManager implements RuntimeConfigurationManager { 12 | private final RuntimeConfiguration runtimeConfiguration; 13 | 14 | @Override 15 | public RuntimeConfiguration getRuntimeConfiguration() { 16 | return runtimeConfiguration; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/base/ErrorWebsocketResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket.base; 5 | 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.Getter; 9 | import lombok.Setter; 10 | import lombok.ToString; 11 | 12 | @Setter 13 | @Getter 14 | @ToString(callSuper = true) 15 | @EqualsAndHashCode(callSuper = true) 16 | public class ErrorWebsocketResponse extends WebsocketResponse { 17 | 18 | @JsonProperty(value = "ErrorMessage") 19 | private String errorMessage; 20 | } 21 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/process/builder/ProcessBuilderWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.process.builder; 5 | 6 | import com.amazon.gamelift.agent.model.exception.BadExecutablePathException; 7 | 8 | import java.util.Map; 9 | 10 | public interface ProcessBuilderWrapper { 11 | 12 | /** 13 | * Creates an underlying process with the environment variables set. Returns a java.lang Process. 14 | * @param environmentVariables 15 | * @return 16 | */ 17 | Process buildProcess(Map environmentVariables) throws BadExecutablePathException; 18 | } 19 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/command/CommandTransform.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.command; 5 | 6 | import com.amazon.gamelift.agent.model.GameProcessConfiguration; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * Interface for CommandTransform. Implementations configure process launch commands for various operating systems 12 | */ 13 | public interface CommandTransform { 14 | 15 | /** 16 | * Resolve the commandLine to execute the delegated command. 17 | * 18 | * @param processConfiguration 19 | */ 20 | List getFullCommandFromConfig(GameProcessConfiguration processConfiguration); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/exception/NotFoundException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.exception; 5 | 6 | /** 7 | * NotFoundException 8 | */ 9 | public class NotFoundException extends AgentException { 10 | 11 | /** 12 | * Throws NotFoundException with message and throwable 13 | */ 14 | public NotFoundException(final String message, final Throwable exception) { 15 | super(message, exception, true); 16 | } 17 | 18 | /** 19 | * Throws NotFoundException with message 20 | */ 21 | public NotFoundException(final String message) { 22 | super(message, true); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/gamelift/GetComputeAuthTokenResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.gamelift; 5 | 6 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketResponse; 7 | import lombok.Builder; 8 | import lombok.Data; 9 | import lombok.NonNull; 10 | 11 | import java.time.Instant; 12 | 13 | @Data 14 | @Builder 15 | public class GetComputeAuthTokenResponse extends WebsocketResponse { 16 | @NonNull private final String fleetId; 17 | @NonNull private final String computeName; 18 | @NonNull private final String authToken; 19 | @NonNull private final Instant expirationTimeEpochMillis; 20 | } 21 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/ForceExitProcessMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket; 5 | 6 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketResponse; 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.Setter; 11 | import lombok.ToString; 12 | 13 | @Setter 14 | @Getter 15 | @ToString(callSuper = true) 16 | @EqualsAndHashCode(callSuper = true) 17 | public class ForceExitProcessMessage extends WebsocketResponse { 18 | 19 | @JsonProperty(value = "ProcessId") 20 | private String processId; 21 | } 22 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/constants/LogCredentials.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.constants; 5 | 6 | import lombok.Getter; 7 | import lombok.NonNull; 8 | import lombok.RequiredArgsConstructor; 9 | 10 | /** 11 | * The values of this enum specify which source to use for credentials when uploading logs. 12 | * The selection is provided via CLI input, with a default of fleet-role. 13 | */ 14 | @Getter 15 | @RequiredArgsConstructor 16 | public enum LogCredentials { 17 | FLEET_ROLE("fleet-role"), 18 | DEFAULT_PROVIDER_CHAIN("default-provider-chain"); 19 | 20 | @NonNull 21 | private final String value; 22 | } 23 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/exception/NotFinishedException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.exception; 5 | 6 | /** 7 | * NotFinishedException 8 | */ 9 | public class NotFinishedException extends AgentException { 10 | 11 | /** 12 | * Creates NotFinishedException with message and throwable 13 | */ 14 | public NotFinishedException(final String message, final Throwable exception) { 15 | super(message, exception, true); 16 | } 17 | 18 | /** 19 | * Creates NotFinishedException with message 20 | */ 21 | public NotFinishedException(final String message) { 22 | super(message, true); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/exception/UnauthorizedException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.exception; 5 | 6 | /** 7 | * UnauthorizedException 8 | */ 9 | public class UnauthorizedException extends AgentException { 10 | 11 | /** 12 | * Creates UnauthorizedException with message and throwable 13 | */ 14 | public UnauthorizedException(final String message, final Throwable exception) { 15 | super(message, exception, false); 16 | } 17 | 18 | /** 19 | * Creates UnauthorizedException with message 20 | */ 21 | public UnauthorizedException(final String message) { 22 | super(message, false); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/DescribeRuntimeConfigurationRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket; 5 | 6 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketRequest; 7 | import com.amazon.gamelift.agent.model.constants.WebSocketActions; 8 | 9 | /** 10 | * Websocket Request for DescribeRuntimeConfiguration 11 | */ 12 | public class DescribeRuntimeConfigurationRequest extends WebsocketRequest { 13 | /** 14 | * Constructor for DescribeRuntimeConfigurationRequest 15 | */ 16 | public DescribeRuntimeConfigurationRequest() { 17 | setAction(WebSocketActions.DescribeRuntimeConfiguration.name()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/constants/EnvironmentConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.constants; 5 | 6 | public class EnvironmentConstants { 7 | public static final String AMI_ID = "ami-id"; 8 | public static final String HOST_ID_KEY = "hostID"; 9 | public static final String HOST_NAME_KEY = "hostName"; 10 | public static final String HOST_PUBLICIPV4_KEY = "hostPublicIPv4"; 11 | public static final String HOST_INSTANCE_TYPE_KEY = "hostInstanceType"; 12 | public static final String INSTANCE_HOSTNAME_KEY = "/latest/meta-data/public-hostname"; 13 | public static final String INSTANCE_PUBLICIPV4_KEY = "/latest/meta-data/public-ipv4"; 14 | } 15 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/NotifyGameSessionActivatedMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket; 5 | 6 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketMessage; 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.ToString; 11 | 12 | @Data 13 | @ToString(callSuper = true) 14 | @EqualsAndHashCode(callSuper = true) 15 | public class NotifyGameSessionActivatedMessage extends WebsocketMessage { 16 | @JsonProperty("ProcessId") 17 | private String processId; 18 | 19 | @JsonProperty("GameSessionId") 20 | private String gameSessionId; 21 | } 22 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/exception/InvalidRequestException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.exception; 5 | 6 | /** 7 | * InvalidRequestException 8 | */ 9 | public class InvalidRequestException extends AgentException { 10 | 11 | /** 12 | * Creates InvalidRequestException with message and throwable 13 | */ 14 | public InvalidRequestException(final String message, final Throwable exception) { 15 | super(message, exception, false); 16 | } 17 | 18 | /** 19 | * Creates InvalidRequestException with message 20 | */ 21 | public InvalidRequestException(final String message) { 22 | super(message, false); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/exception/BadExecutablePathException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.exception; 5 | 6 | /** 7 | * BadExecutablePathException 8 | */ 9 | public class BadExecutablePathException extends AgentException { 10 | /** 11 | * Creates BadExecutablePathException with message and exception 12 | */ 13 | public BadExecutablePathException(final String message, final Throwable exception) { 14 | super(message, exception, false); 15 | } 16 | 17 | /** 18 | * Creates BadExecutablePathException with message 19 | */ 20 | public BadExecutablePathException(final String message) { 21 | super(message, false); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/NotifyProcessRegisteredMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket; 5 | 6 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketMessage; 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.ToString; 11 | 12 | import java.util.List; 13 | 14 | @Data 15 | @ToString(callSuper = true) 16 | @EqualsAndHashCode(callSuper = true) 17 | public class NotifyProcessRegisteredMessage extends WebsocketMessage { 18 | @JsonProperty("ProcessId") 19 | private String processId; 20 | 21 | @JsonProperty("LogPaths") 22 | private List logPaths; 23 | } 24 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/constants/GameLiftCredentials.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.constants; 5 | 6 | import lombok.Getter; 7 | import lombok.NonNull; 8 | import lombok.RequiredArgsConstructor; 9 | 10 | /** 11 | * The values of this enum specify which source to use for credentials when creating an Amazon GameLift client. 12 | * The selection is provided via CLI input, with a default of INSTANCE_PROFILE. 13 | */ 14 | @Getter 15 | @RequiredArgsConstructor 16 | public enum GameLiftCredentials { 17 | INSTANCE_PROFILE("instance-profile"), 18 | ENVIRONMENT_VARIABLE("environment-variable"), 19 | CONTAINER("container"); 20 | 21 | @NonNull 22 | private final String value; 23 | } 24 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/ForceExitServerProcessMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket; 5 | 6 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketResponse; 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.Setter; 11 | import lombok.ToString; 12 | 13 | @Setter 14 | @Getter 15 | @ToString(callSuper = true) 16 | @EqualsAndHashCode(callSuper = true) 17 | public class ForceExitServerProcessMessage extends WebsocketResponse { 18 | 19 | @JsonProperty(value = "ProcessId") 20 | private String processId; 21 | 22 | @JsonProperty(value = "TerminationReason") 23 | private String terminationReason; 24 | } 25 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/gamelift/RegisterComputeResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.gamelift; 5 | 6 | import java.util.Date; 7 | 8 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketResponse; 9 | import lombok.Builder; 10 | import lombok.Data; 11 | import lombok.NonNull; 12 | 13 | @Data 14 | @Builder 15 | public class RegisterComputeResponse extends WebsocketResponse { 16 | @NonNull private final String fleetId; 17 | @NonNull private final String computeName; 18 | @NonNull private final String sdkWebsocketEndpoint; 19 | @NonNull private final String agentWebsocketEndpoint; 20 | private final String status; 21 | private final String location; 22 | private final Date creationTime; 23 | } 24 | -------------------------------------------------------------------------------- /src/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | %d{dd MMM yyyy HH:mm:ss,SSS} %highlight{[%p]} (%t) %c: %m%n 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/StartComputeTerminationMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket; 5 | 6 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketMessage; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.Getter; 9 | import lombok.Setter; 10 | import lombok.ToString; 11 | 12 | @Setter 13 | @Getter 14 | @ToString(callSuper = true) 15 | @EqualsAndHashCode(callSuper = true) 16 | public class StartComputeTerminationMessage extends WebsocketMessage { 17 | /** 18 | * No additional fields are required for this message beyond what is on WebsocketMessage. This is created for 19 | * clarity of code and to have it in place for when/if more information is added to this message. 20 | */ 21 | } 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | 3 | # 1.0.3 (12/30/2024) 4 | - Add termination reason to NotifyServerProcessTermination message 5 | - Enable compute registration by default when running on non-container compute 6 | - Minor bug fixes 7 | 8 | # 1.0.2 (11/04/2024) 9 | - Update compute registration for container fleets 10 | - Skip spot termination checks when running on non-spot or non-EC2 instances 11 | - Add option to use AWS default credential provider chain to upload logs 12 | - Remove DeregisterCompute call for container compute when termination is started 13 | - Minor bug fixes 14 | 15 | # 1.0.1 (07/23/2024) 16 | - Add support for UNKNOWN_WINDOWS and UNKNOWN_LINUX platforms 17 | - Add initial GitHub Action to compile/test Agent with Maven automatically 18 | - Minor bug fixes 19 | 20 | # 1.0.0 (04/24/2024) 21 | - Initial public release of amazon-gamelift-agent -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | Note: This is just a template, so feel free to use/remove the unnecessary things 2 | 3 | ### Description 4 | - Type: Bug | Enhancement\Feature Request | Question 5 | - Priority: Blocker | Major | Minor 6 | 7 | --------------------------------------------------------------- 8 | ## Bug 9 | 10 | **Expected behavior** 11 | 12 | **Actual behavior** 13 | 14 | **Steps to reproduce** 15 | 16 | ---------------------------------------------------------------- 17 | ## Enhancement\Feature Request 18 | 19 | **Justification - why does the library need this feature?** 20 | 21 | **Suggested enhancement** 22 | 23 | ----------------------------------------------------------------- 24 | 25 | ## Question 26 | 27 | - [ ] Read through all relevant documentation. 28 | - [ ] Make sure that you have searched through previous issues (incase this is to be a duplicate). -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/windows/MoreNetapi32.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.windows; 5 | 6 | import com.sun.jna.Native; 7 | import com.sun.jna.Structure; 8 | import com.sun.jna.platform.win32.Netapi32; 9 | import com.sun.jna.ptr.IntByReference; 10 | import com.sun.jna.win32.W32APIOptions; 11 | 12 | /** 13 | * Interface for More Netapi32 14 | */ 15 | public interface MoreNetapi32 extends Netapi32 { 16 | MoreNetapi32 INSTANCE = Native.load("Netapi32", 17 | MoreNetapi32.class, 18 | W32APIOptions.UNICODE_OPTIONS); 19 | 20 | int NetUserSetInfo(final String serverName, 21 | final String userName, 22 | final int level, Structure buffer, 23 | final IntByReference error); 24 | } 25 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/exception/NotReadyException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.exception; 5 | 6 | /** 7 | * NotReadyException 8 | */ 9 | public class NotReadyException extends AgentException { 10 | 11 | /** 12 | * Creates NotReadyException with message and throwable 13 | */ 14 | public NotReadyException(final String message, final Throwable exception) { 15 | super(message, exception, true); 16 | } 17 | 18 | /** 19 | * Creates NotReadyException with message 20 | */ 21 | public NotReadyException(final String message) { 22 | super(message, true); 23 | } 24 | 25 | /** 26 | * Creates NotReadyException 27 | */ 28 | public NotReadyException() { 29 | super(true); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/RuntimeConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model; 5 | 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import lombok.Builder; 8 | import lombok.NonNull; 9 | import lombok.Value; 10 | import lombok.extern.jackson.Jacksonized; 11 | 12 | import java.util.List; 13 | 14 | @Builder 15 | @Jacksonized 16 | @Value 17 | public class RuntimeConfiguration { 18 | @JsonProperty("GameSessionActivationTimeoutSeconds") 19 | private final Integer gameSessionActivationTimeoutSeconds; 20 | @JsonProperty("MaxConcurrentGameSessionActivations") 21 | private final Integer maxConcurrentGameSessionActivations; 22 | @NonNull 23 | @JsonProperty("ServerProcesses") 24 | private final List serverProcesses; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/exception/ThrottlingException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.exception; 5 | 6 | /** 7 | * ThrottlingException 8 | */ 9 | public class ThrottlingException extends AgentException { 10 | 11 | /** 12 | * Creates ThrottlingException with message and throwable 13 | */ 14 | public ThrottlingException(final String message, final Throwable exception) { 15 | super(message, exception, true); 16 | } 17 | 18 | /** 19 | * Creates ThrottlingException with message 20 | */ 21 | public ThrottlingException(final String message) { 22 | super(message, true); 23 | } 24 | 25 | /** 26 | * Creates ThrottlingException 27 | */ 28 | public ThrottlingException() { 29 | super(true); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/utils/ExecutorServiceSafeRunnableTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.utils; 5 | 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | 10 | @ExtendWith(MockitoExtension.class) 11 | public class ExecutorServiceSafeRunnableTest { 12 | 13 | @Test 14 | public void GIVEN_runnableThrowsException_WHEN_run_THEN_suppressExceptions() { 15 | // GIVEN 16 | final Runnable runnableThatThrows = () -> { 17 | throw new RuntimeException("This should be caught and suppressed! O_O"); 18 | }; 19 | 20 | // WHEN 21 | new ExecutorServiceSafeRunnable(runnableThatThrows).run(); 22 | 23 | // THEN - no exception 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/base/WebsocketRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket.base; 5 | 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.Getter; 9 | import lombok.NonNull; 10 | import lombok.Setter; 11 | import lombok.ToString; 12 | 13 | import java.util.UUID; 14 | 15 | @Setter 16 | @Getter 17 | @ToString(callSuper = true) 18 | @EqualsAndHashCode(callSuper = true) 19 | public class WebsocketRequest extends WebsocketMessage { 20 | @NonNull 21 | @JsonProperty(value = "RequestId") 22 | private String requestId; 23 | 24 | /** 25 | * Constructor for WebsocketRequest 26 | */ 27 | public WebsocketRequest() { 28 | requestId = UUID.randomUUID().toString(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/RefreshConnectionMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket; 5 | 6 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketMessage; 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.ToString; 11 | 12 | import java.time.Instant; 13 | 14 | @Data 15 | @ToString(callSuper = true) 16 | @EqualsAndHashCode(callSuper = true) 17 | public class RefreshConnectionMessage extends WebsocketMessage { 18 | @JsonProperty("RefreshConnectionEndpoint") 19 | private String refreshConnectionEndpoint; 20 | @ToString.Exclude 21 | @JsonProperty("AuthToken") 22 | private String authToken; 23 | @JsonProperty(value = "ExpirationTime") 24 | private Instant expirationTime; 25 | } 26 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/exception/InternalServiceException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.exception; 5 | 6 | /** 7 | * InternalServiceException 8 | */ 9 | public class InternalServiceException extends AgentException { 10 | 11 | /** 12 | * Creates InternalServiceException with message and throwable 13 | */ 14 | public InternalServiceException(final String message, final Throwable exception) { 15 | super(message, exception, true); 16 | } 17 | 18 | /** 19 | * Creates InternalServiceException with message 20 | */ 21 | public InternalServiceException(final String message) { 22 | super(message, true); 23 | } 24 | 25 | /** 26 | * Creates InternalServiceException 27 | */ 28 | public InternalServiceException() { 29 | super(true); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/maven_compile_and_test.yml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | 3 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 4 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 5 | 6 | name: Compile and Test GameLift Agent with Maven 7 | 8 | on: 9 | push: 10 | branches: [ "main" ] 11 | pull_request: 12 | branches: [ "main" ] 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up JDK 17 22 | uses: actions/setup-java@v4 23 | with: 24 | java-version: '17' 25 | distribution: 'corretto' 26 | architecture: 'x64' 27 | cache: maven 28 | - name: Build with Maven 29 | run: mvn clean compile assembly:single test -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/SendHeartbeatResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket; 5 | 6 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketResponse; 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.Setter; 11 | import lombok.ToString; 12 | 13 | import java.util.List; 14 | 15 | @Setter 16 | @Getter 17 | @ToString(callSuper = true) 18 | @EqualsAndHashCode(callSuper = true) 19 | public class SendHeartbeatResponse extends WebsocketResponse { 20 | @JsonProperty(value = "Status") 21 | private String status; 22 | @JsonProperty(value = "UnhealthyProcesses") 23 | private List unhealthyProcesses; 24 | @JsonProperty(value = "UnregisteredProcesses") 25 | private List unregisteredProcesses; 26 | } 27 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/process/destroyer/ProcessDestroyerFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.process.destroyer; 5 | 6 | import com.amazon.gamelift.agent.model.OperatingSystem; 7 | 8 | public class ProcessDestroyerFactory { 9 | /** 10 | * Return ProcessDestroyer based on OS 11 | * @param operatingSystem 12 | * @return 13 | */ 14 | public static ProcessDestroyer getProcessDestroyer(final OperatingSystem operatingSystem) { 15 | return switch (operatingSystem.getOperatingSystemFamily()) { 16 | case LINUX -> new LinuxProcessDestroyer(operatingSystem); 17 | case WINDOWS -> new WindowsProcessDestroyer(operatingSystem); 18 | default -> throw new IllegalArgumentException("Failed to find underlying process destroyer for OS " 19 | + operatingSystem); 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/base/WebsocketResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket.base; 5 | 6 | import org.apache.http.HttpStatus; 7 | import com.fasterxml.jackson.annotation.JsonIgnore; 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.Getter; 11 | import lombok.Setter; 12 | import lombok.ToString; 13 | 14 | @Setter 15 | @Getter 16 | @ToString(callSuper = true) 17 | @EqualsAndHashCode(callSuper = true) 18 | public class WebsocketResponse extends WebsocketMessage { 19 | @JsonProperty(value = "RequestId") 20 | private String requestId; 21 | 22 | @JsonProperty(value = "StatusCode") 23 | private Integer statusCode; 24 | 25 | @JsonIgnore 26 | public boolean isErrorResponse() { 27 | return (statusCode != null && statusCode != HttpStatus.SC_OK); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/websocket/handlers/DefaultHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.websocket.handlers; 5 | 6 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketResponse; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | import javax.inject.Inject; 11 | 12 | @Slf4j 13 | public class DefaultHandler extends MessageHandler { 14 | 15 | /** 16 | * Constructor for DefaultHandler 17 | * See GameLiftAgentWebSocketListener for message routing by Action 18 | * @param objectMapper 19 | */ 20 | @Inject 21 | public DefaultHandler(final ObjectMapper objectMapper) { 22 | super(WebsocketResponse.class, objectMapper); 23 | } 24 | 25 | @Override 26 | public void handle(final WebsocketResponse message) { 27 | log.error("Action {} not supported", message.getAction()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/model/ProcessTerminationReasonTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertNull; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.junit.jupiter.MockitoExtension; 11 | 12 | @ExtendWith(MockitoExtension.class) 13 | public class ProcessTerminationReasonTest { 14 | 15 | @Test 16 | public void GIVEN_reasonWithEventCode_WHEN_getEventCode_THEN_returnsCodeString() { 17 | assertEquals("SERVER_PROCESS_CRASHED", ProcessTerminationReason.SERVER_PROCESS_CRASHED.getEventCode()); 18 | } 19 | 20 | @Test 21 | public void GIVEN_reasonWithoutEventCode_WHEN_getEventCode_THEN_returnsNull() { 22 | assertNull(ProcessTerminationReason.COMPUTE_SHUTTING_DOWN.getEventCode()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/component/GameLiftAgentComponent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.component; 5 | 6 | import com.amazon.gamelift.agent.Agent; 7 | import com.amazon.gamelift.agent.module.ClientModule; 8 | import com.amazon.gamelift.agent.module.ConfigModule; 9 | import com.amazon.gamelift.agent.module.ProcessModule; 10 | import com.amazon.gamelift.agent.module.ThreadingModule; 11 | import dagger.Component; 12 | 13 | import javax.inject.Singleton; 14 | 15 | /** 16 | * Interface for global agent dagger dependency injection. Dagger suggests having a single component 17 | * used by your service at runtime. 18 | */ 19 | @Singleton 20 | @Component(modules = {ConfigModule.class, ClientModule.class, ProcessModule.class, ThreadingModule.class}) 21 | public interface GameLiftAgentComponent { 22 | /** 23 | * Builds a GameLiftAgent (singleton instance) 24 | * @return 25 | */ 26 | Agent buildGameLiftAgent(); 27 | } 28 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/exception/AgentException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.exception; 5 | 6 | import lombok.Getter; 7 | 8 | /** 9 | * Base exception type for all exceptions in the GameLiftAgent, which allows us to identify 10 | * which exceptions are retryable & which aren't 11 | */ 12 | @Getter 13 | public class AgentException extends Exception { 14 | 15 | private final boolean isRetryable; 16 | 17 | protected AgentException(final String message, final Throwable exception, final boolean isRetryable) { 18 | super(message, exception); 19 | this.isRetryable = isRetryable; 20 | } 21 | 22 | protected AgentException(final String message, final boolean isRetryable) { 23 | super(message); 24 | this.isRetryable = isRetryable; 25 | } 26 | 27 | protected AgentException(final boolean isRetryable) { 28 | super(); 29 | this.isRetryable = isRetryable; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/websocket/SdkWebsocketEndpointProviderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.websocket; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | @ExtendWith(MockitoExtension.class) 14 | public class SdkWebsocketEndpointProviderTest { 15 | 16 | @Test 17 | public void GIVEN_noEndpointSet_WHEN_getSdkWebsocketEndpoint_THEN_throwsException() { 18 | assertThrows(IllegalStateException.class, () -> new SdkWebsocketEndpointProvider().getSdkWebsocketEndpoint()); 19 | } 20 | 21 | @Test 22 | public void GIVEN_endpointSet_WHEN_getSdkWebsocketEndpoint_THEN_returnsException() { 23 | SdkWebsocketEndpointProvider provider = new SdkWebsocketEndpointProvider(); 24 | provider.setSdkWebsocketEndpoint("Test"); 25 | assertEquals("Test", provider.getSdkWebsocketEndpoint()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/logging/AgentLogFileFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.logging; 5 | 6 | import java.io.File; 7 | import java.io.FileFilter; 8 | import java.nio.file.Files; 9 | import javax.inject.Inject; 10 | 11 | import com.amazon.gamelift.agent.manager.LogConfigurationManager; 12 | 13 | /** 14 | * A basic log file filter that helps the GameLift agent filter out undesirable files in the agent Log directory, 15 | * so that only actual log files get uploaded 16 | */ 17 | public class AgentLogFileFilter implements FileFilter { 18 | 19 | /** 20 | * Empty constructor for AgentLogFileFilter 21 | */ 22 | @Inject 23 | public AgentLogFileFilter() { } 24 | 25 | @Override 26 | public boolean accept(final File file) { 27 | final boolean baseNameMatchesExpected = 28 | file.getName().startsWith(LogConfigurationManager.APPLICATION_LOG_FILE_BASENAME); 29 | 30 | return (file.isFile() 31 | && !file.isDirectory() 32 | && baseNameMatchesExpected 33 | && !Files.isSymbolicLink(file.toPath())); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/command/LinuxCommandTransform.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.command; 5 | 6 | import com.amazon.gamelift.agent.model.GameProcessConfiguration; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * LinuxCommandTransform is used to generate a process launch command for Linux operating system. 13 | */ 14 | public class LinuxCommandTransform implements CommandTransform { 15 | 16 | /** 17 | * {@inheritDoc} 18 | * 19 | * Setsid to force the new process into its own session ID (SID) and process group ID (PGID). 20 | * This allows us to kill process trees more efficiently by using the PGID. 21 | */ 22 | @Override 23 | public List getFullCommandFromConfig(final GameProcessConfiguration processConfiguration) { 24 | final List commandComponentList = new ArrayList<>(); 25 | 26 | commandComponentList.add("setsid"); 27 | 28 | commandComponentList.add(processConfiguration.getLaunchPath()); 29 | commandComponentList.addAll(processConfiguration.getParameters()); 30 | 31 | return commandComponentList; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/websocket/SdkWebsocketEndpointProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.websocket; 5 | 6 | import lombok.AccessLevel; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | 10 | import javax.inject.Singleton; 11 | 12 | /** 13 | * Simple singleton provider class for the SDK Websocket Endpoint. 14 | * This should only ever be set once when the compute is registered when the GameLiftAgent starts, 15 | * so an exception should ideally never be thrown by the getter. 16 | */ 17 | @Singleton 18 | @NoArgsConstructor 19 | public class SdkWebsocketEndpointProvider { 20 | 21 | @Setter(AccessLevel.PACKAGE) 22 | private String sdkWebsocketEndpoint = null; 23 | 24 | /** 25 | * Getter for sdkWebsocketEndpoint. IllegalStateException if null 26 | * @return 27 | */ 28 | public String getSdkWebsocketEndpoint() { 29 | if (sdkWebsocketEndpoint == null) { 30 | throw new IllegalStateException("Attempted to retrieve SDK Websocket Endpoint " 31 | + "before it was set by RegisterCompute"); 32 | } 33 | 34 | return sdkWebsocketEndpoint; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/process/builder/ProcessBuilderFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.process.builder; 5 | 6 | import com.amazon.gamelift.agent.model.GameProcessConfiguration; 7 | import com.amazon.gamelift.agent.model.OperatingSystem; 8 | 9 | public class ProcessBuilderFactory { 10 | 11 | /** 12 | * Return ProcessBuilderWrapper based on OS 13 | * @param processConfiguration 14 | * @param operatingSystem 15 | * @return 16 | */ 17 | public static ProcessBuilderWrapper getProcessBuilder(final GameProcessConfiguration processConfiguration, 18 | final OperatingSystem operatingSystem) { 19 | return switch (operatingSystem.getOperatingSystemFamily()) { 20 | case LINUX -> 21 | new LinuxProcessBuilderWrapper(processConfiguration, operatingSystem); 22 | case WINDOWS -> 23 | new WindowsProcessBuilderWrapper(processConfiguration, operatingSystem); 24 | default -> throw new IllegalArgumentException("Failed to find underlying process builder for OS " 25 | + operatingSystem); 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/windows/MoreLMAccess.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.windows; 5 | 6 | import com.sun.jna.Pointer; 7 | import com.sun.jna.Structure; 8 | import com.sun.jna.WString; 9 | import com.sun.jna.platform.win32.LMAccess; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * Interface for More LMAccess * 15 | */ 16 | public interface MoreLMAccess extends LMAccess { 17 | int NERR_Success = 0; 18 | 19 | int LEVEL_USER_INFO_1003 = 1003; 20 | 21 | /** 22 | * The USER_INFO_1003 structure contains a user password. This information level is valid only when NetUserSetInfo 23 | * function is called. 24 | * 25 | * @see MSDN 26 | */ 27 | class USER_INFO_1003 extends Structure { 28 | public WString usri1003_password; 29 | 30 | public USER_INFO_1003() { 31 | } 32 | 33 | public USER_INFO_1003(final Pointer memory) { 34 | super(memory); 35 | this.read(); 36 | } 37 | 38 | protected List getFieldOrder() { 39 | return List.of("usri1003_password"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/utils/ExecutorServiceSafeRunnable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.utils; 5 | 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | /** 10 | * Helper class to suppress all exceptions from a given Runnable. ScheduledExecutorServices have particularly bad 11 | * behavior where all future scheduled runs will be cancelled if an unexpected exception gets thrown in the runnable. 12 | * Using this helper avoids executors getting into a nasty unrecoverable state because of a single exception. 13 | */ 14 | @Slf4j 15 | @RequiredArgsConstructor 16 | public class ExecutorServiceSafeRunnable implements Runnable { 17 | 18 | private final Runnable childRunnable; 19 | 20 | @Override 21 | public void run() { 22 | try { 23 | this.childRunnable.run(); 24 | } catch (final Exception e) { 25 | // Do not re-throw exceptions from this runnable. If an unhandled exception is thrown in the runnable 26 | // of a ScheduledExecutorService, all future executions will be silently stopped. 27 | log.error("Suppressing unexpected exception to prevent impact to future ExecutorService runs", e); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/FleetRoleCredentialsConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model; 5 | 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import lombok.Builder; 8 | import lombok.Value; 9 | import lombok.extern.jackson.Jacksonized; 10 | 11 | @Builder 12 | @Jacksonized 13 | @Value 14 | public class FleetRoleCredentialsConfiguration { 15 | @JsonProperty("AssumedRoleUserArn") 16 | private String assumedRoleUserArn; 17 | @JsonProperty("AssumedRoleId") 18 | private String assumedRoleId; 19 | @JsonProperty("AccessKeyId") 20 | private String accessKeyId; 21 | @JsonProperty("SecretAccessKey") 22 | private String secretAccessKey; 23 | @JsonProperty("SessionToken") 24 | private String sessionToken; 25 | @JsonProperty("Expiration") 26 | private Long expiration; 27 | 28 | /** 29 | * Custom toString for fleetRoleCredentialsConfiguration 30 | * @return 31 | */ 32 | @Override 33 | public String toString() { 34 | return String.format("AssumedRoleUserArn: [%s] AssumedRoleId: [%s] Expiration: [%s]", 35 | getAssumedRoleUserArn(), 36 | getAssumedRoleId(), 37 | getExpiration()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/OperatingSystemFamily.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model; 5 | 6 | import lombok.Getter; 7 | 8 | /** 9 | * Specifies Amazon GameLift's supported Operating System families. Used to classify the operating system on which 10 | * GameLiftAgent is running. 11 | * 12 | */ 13 | @Getter 14 | public enum OperatingSystemFamily { 15 | WINDOWS("Windows"), 16 | LINUX("Unix"), 17 | INVALID("Invalid"); 18 | 19 | private final String osFamilyName; 20 | 21 | OperatingSystemFamily(final String osFamilyName) { 22 | this.osFamilyName = osFamilyName; 23 | } 24 | 25 | /** 26 | * Convert a CloudFormation parameter name to an OperatingSystemFamily. 27 | * @param osFamilyName The name to convert 28 | * @return a {@link OperatingSystemFamily} 29 | */ 30 | public static OperatingSystemFamily fromOsFamilyName(final String osFamilyName) { 31 | for (final OperatingSystemFamily family : values()) { 32 | if (family.osFamilyName.equals(osFamilyName)) { 33 | return family; 34 | } 35 | } 36 | throw new IllegalArgumentException(String.format("No family found for the value %s", osFamilyName)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/GetFleetRoleCredentialsRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket; 5 | 6 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketRequest; 7 | import com.amazon.gamelift.agent.model.constants.WebSocketActions; 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | import lombok.Data; 10 | import lombok.EqualsAndHashCode; 11 | import lombok.ToString; 12 | import org.apache.commons.lang3.RandomStringUtils; 13 | 14 | @Data 15 | @ToString(callSuper = true) 16 | @EqualsAndHashCode(callSuper = true) 17 | public class GetFleetRoleCredentialsRequest extends WebsocketRequest { 18 | @JsonProperty("RoleSessionName") 19 | private String roleSessionName; 20 | 21 | private static final String ROLE_SESSION_NAME_PREFIX = "GameLiftAgentSession"; 22 | private static final String ROLE_SESSION_SUFFIX = RandomStringUtils.randomAlphanumeric(5); 23 | /** 24 | * Constructor for GetFleetRoleCredentialsRequest 25 | */ 26 | public GetFleetRoleCredentialsRequest() { 27 | this.roleSessionName = String.format("%s-%s", ROLE_SESSION_NAME_PREFIX, ROLE_SESSION_SUFFIX); 28 | setAction(WebSocketActions.GetFleetRoleCredentials.name()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/command/LinuxCommandTransformTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.command; 5 | 6 | import com.amazon.gamelift.agent.model.GameProcessConfiguration; 7 | 8 | import java.util.List; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | 13 | class LinuxCommandTransformTest { 14 | 15 | private static final GameProcessConfiguration PROCESS_CONFIG = GameProcessConfiguration.builder() 16 | .concurrentExecutions(1) 17 | .launchPath("testCommand") 18 | .parameters("--parameter1 --parameter2") 19 | .build(); 20 | 21 | private CommandTransform commandTransform; 22 | 23 | @BeforeEach 24 | public void setup() { 25 | commandTransform = new LinuxCommandTransform(); 26 | } 27 | 28 | @Test 29 | public void GIVEN_validInput_WHEN_getFullCommandFromConfig_THEN_returnLinuxCommand() { 30 | // Given 31 | 32 | // When 33 | List result = commandTransform.getFullCommandFromConfig(PROCESS_CONFIG); 34 | 35 | // Then 36 | assertEquals(List.of("setsid", "testCommand", "--parameter1", "--parameter2"), result); 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/GameProcessConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model; 5 | 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import lombok.Builder; 8 | import lombok.NonNull; 9 | import lombok.Value; 10 | import lombok.extern.jackson.Jacksonized; 11 | 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.Objects; 16 | 17 | /** 18 | * Model that represents the ServerProcess field in the Amazon GameLift RuntimeConfiguration 19 | */ 20 | @Builder 21 | @Jacksonized 22 | @Value 23 | public class GameProcessConfiguration { 24 | private static final int DEFAULT_CONCURRENT_EXECUTIONS = 1; 25 | 26 | @NonNull 27 | @JsonProperty("ConcurrentExecutions") 28 | private final Integer concurrentExecutions; 29 | @NonNull 30 | @JsonProperty("LaunchPath") 31 | private final String launchPath; 32 | @JsonProperty("Parameters") 33 | private final String parameters; 34 | public Integer getConcurrentExecutions() { 35 | return Objects.isNull(concurrentExecutions) ? DEFAULT_CONCURRENT_EXECUTIONS : concurrentExecutions; 36 | } 37 | 38 | public List getParameters() { 39 | return (parameters == null) ? new ArrayList<>() : Arrays.asList(parameters.split(" ")); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/DescribeRuntimeConfigurationResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket; 5 | 6 | import com.amazon.gamelift.agent.model.GameProcessConfiguration; 7 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketResponse; 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | 10 | import java.util.List; 11 | 12 | import lombok.Builder; 13 | import lombok.EqualsAndHashCode; 14 | import lombok.NonNull; 15 | import lombok.ToString; 16 | import lombok.Value; 17 | import lombok.extern.jackson.Jacksonized; 18 | 19 | /** 20 | * Response for DescribeRuntimeConfiguration. This is the model for the immediate json output from the websocket. 21 | * Other classes should use RuntimeConfiguration (pared-down model) 22 | */ 23 | @Builder 24 | @Jacksonized 25 | @ToString(callSuper = true) 26 | @EqualsAndHashCode(callSuper = true) 27 | @Value 28 | public class DescribeRuntimeConfigurationResponse extends WebsocketResponse { 29 | @JsonProperty("GameSessionActivationTimeoutSeconds") 30 | private Integer gameSessionActivationTimeoutSeconds; 31 | @JsonProperty("MaxConcurrentGameSessionActivations") 32 | private Integer maxConcurrentGameSessionActivations; 33 | @NonNull 34 | @JsonProperty("ServerProcesses") 35 | private List serverProcesses; 36 | } 37 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/windows/MoreAdvapi32.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.windows; 5 | 6 | import com.sun.jna.Native; 7 | import com.sun.jna.Pointer; 8 | import com.sun.jna.platform.win32.Advapi32; 9 | import com.sun.jna.platform.win32.WinBase; 10 | import com.sun.jna.platform.win32.WinNT; 11 | import com.sun.jna.win32.W32APIOptions; 12 | 13 | public interface MoreAdvapi32 extends Advapi32 { 14 | MoreAdvapi32 INSTANCE = Native.load("Advapi32", 15 | MoreAdvapi32.class, 16 | W32APIOptions.UNICODE_OPTIONS); 17 | 18 | boolean CreateProcessAsUser(final WinNT.HANDLE hToken, 19 | final String lpApplicationName, 20 | final String lpCommandLine, 21 | final WinBase.SECURITY_ATTRIBUTES lpProcessAttributes, 22 | final WinBase.SECURITY_ATTRIBUTES lpThreadAttributes, 23 | final boolean bInheritHandles, 24 | final int dwCreationFlags, 25 | final Pointer lpEnvironment, 26 | final String lpCurrentDirectory, 27 | final WinBase.STARTUPINFO lpStartupInfo, 28 | final WinBase.PROCESS_INFORMATION lpProcessInformation); 29 | } 30 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/model/OperatingSystemFamilyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model; 5 | 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertThrows; 12 | 13 | @ExtendWith(MockitoExtension.class) 14 | public class OperatingSystemFamilyTest { 15 | @Test 16 | public void GIVEN_Windows_WHEN_fromOsFamilyName_thenWindowsFamilyReturned() { 17 | // WHEN 18 | final OperatingSystemFamily family = OperatingSystemFamily.fromOsFamilyName("Windows"); 19 | 20 | // THEN 21 | assertEquals(family, OperatingSystemFamily.WINDOWS); 22 | } 23 | 24 | @Test 25 | public void GIVEN_Unix_WHEN_fromOsFamilyName_thenLinuxFamilyReturned() { 26 | // WHEN 27 | final OperatingSystemFamily family = OperatingSystemFamily.fromOsFamilyName("Unix"); 28 | 29 | // THEN 30 | assertEquals(family, OperatingSystemFamily.LINUX); 31 | } 32 | 33 | @Test 34 | public void GIVEN_Invalid_WHEN_fromOsFamilyName_thenInvalidFamilyReturned() { 35 | // WHEN/THEN 36 | assertThrows(IllegalArgumentException.class, () -> OperatingSystemFamily.fromOsFamilyName("tacoTuesday")); 37 | } 38 | } -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/ComputeStatus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model; 5 | 6 | /** 7 | * Enumeration for Compute status. 8 | * 9 | * In GameLiftAgent, this enumeration is used to express the state a Compute is in as follows: 10 | * From State Transition To State 11 | * (GameLiftAgent Starts) --[GameLiftAgent starts]--> (Initializing) 12 | * (Initializing) --[GameLiftAgent starting server process]--> (Activating) 13 | * (Activating) --[At least one GameProcess ready]--> (Active) 14 | * (Active) --[StartComputeTermination message received]--> (Terminating) 15 | */ 16 | 17 | public enum ComputeStatus { 18 | Initializing, // Initial state on GameLiftAgent launch. 19 | Activating, // GameProcess launch has initiated; waiting on first process to start. 20 | Active, // At least one GameProcess is ready. 21 | Terminating, // GameLiftAgent received StartComputeTermination message. 22 | Terminated, // GameLiftAgent exiting after StartComputeTermination message. 23 | Interrupted // GameLiftAgent received spot termination notice and will attempt to terminate. 24 | } 25 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/AgentArgs.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model; 5 | 6 | import com.amazon.gamelift.agent.model.constants.GameLiftCredentials; 7 | 8 | import com.amazon.gamelift.agent.model.constants.LogCredentials; 9 | import lombok.Builder; 10 | import lombok.NonNull; 11 | import lombok.Value; 12 | 13 | import java.time.Instant; 14 | 15 | /** 16 | * Class to encapsulate the commandline arguments for the GameLift agent. 17 | */ 18 | @Builder 19 | @Value 20 | public class AgentArgs { 21 | private RuntimeConfiguration runtimeConfiguration; 22 | @NonNull private String fleetId; 23 | private String computeName; 24 | @NonNull private String region; 25 | private String gameLiftEndpointOverride; 26 | private String gameLiftAgentWebsocketEndpoint; 27 | private String gameLiftSdkWebsocketEndpoint; 28 | private String ipAddress; 29 | private String certificatePath; 30 | private String dnsName; 31 | private String location; 32 | private String concurrentExecutions; 33 | private GameLiftCredentials gameLiftCredentials; 34 | private String gameSessionLogBucket; 35 | private String agentLogBucket; 36 | private String agentLogPath; 37 | private LogCredentials logCredentials; 38 | private Boolean isContainerFleet; 39 | private Boolean enableComputeRegistrationViaAgent; 40 | private Instant heartbeatTimeoutTime; 41 | } 42 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/logging/GameSessionLogPath.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.logging; 5 | 6 | import lombok.Getter; 7 | 8 | @Getter 9 | public class GameSessionLogPath { 10 | private final String sourcePath; 11 | private final String relativePathInZip; 12 | private final String wildcardToGet; 13 | 14 | /** 15 | * Constructor for GameSessionLogPath 16 | * @param sourcePath - Path for the log file to be copied / uploaded 17 | * @param relativePathInZip - Relative path for the log file within the zip file created for upload 18 | */ 19 | public GameSessionLogPath(final String sourcePath, final String relativePathInZip) { 20 | this(sourcePath, relativePathInZip, null); 21 | } 22 | 23 | /** 24 | * Constructor for GameSessionLogPath 25 | * @param sourcePath - Path for the log file to be copied / uploaded 26 | * @param relativePathInZip - Relative path for the log file within the zip file created for upload 27 | * @param wildcardToGet - Wildcard to be expanded into any matching existing log files 28 | */ 29 | public GameSessionLogPath(final String sourcePath, final String relativePathInZip, final String wildcardToGet) { 30 | this.sourcePath = sourcePath; 31 | this.relativePathInZip = relativePathInZip; 32 | this.wildcardToGet = wildcardToGet == null ? null : wildcardToGet.toLowerCase(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/websocket/handlers/StartComputeTerminationHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.websocket.handlers; 5 | 6 | import com.amazon.gamelift.agent.model.websocket.StartComputeTerminationMessage; 7 | import com.amazon.gamelift.agent.manager.ShutdownOrchestrator; 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | import javax.inject.Inject; 12 | import java.time.Instant; 13 | 14 | @Slf4j 15 | public class StartComputeTerminationHandler extends MessageHandler { 16 | 17 | private final ShutdownOrchestrator shutdownOrchestrator; 18 | 19 | /** 20 | * Constructor for StartComputeTerminationHandler 21 | * See GameLiftAgentWebSocketListener for message routing by Action 22 | * @param objectMapper 23 | * @param shutdownOrchestrator 24 | */ 25 | @Inject 26 | public StartComputeTerminationHandler(final ObjectMapper objectMapper, final ShutdownOrchestrator shutdownOrchestrator) { 27 | super(StartComputeTerminationMessage.class, objectMapper); 28 | this.shutdownOrchestrator = shutdownOrchestrator; 29 | } 30 | 31 | @Override 32 | public void handle(final StartComputeTerminationMessage message) { 33 | log.info("StartComputeTermination message received: {}", message); 34 | shutdownOrchestrator.startTermination(Instant.now().plus(ShutdownOrchestrator.DEFAULT_TERMINATION_DEADLINE), false); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/NotifyServerProcessTerminationRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket; 5 | 6 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketRequest; 7 | import com.amazon.gamelift.agent.model.constants.WebSocketActions; 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | import lombok.Builder; 10 | import lombok.Data; 11 | import lombok.EqualsAndHashCode; 12 | import lombok.ToString; 13 | 14 | @Data 15 | @Builder 16 | @ToString(callSuper = true) 17 | @EqualsAndHashCode(callSuper = true) 18 | public class NotifyServerProcessTerminationRequest extends WebsocketRequest { 19 | @JsonProperty(value = "ProcessId") 20 | private final String processId; 21 | @JsonProperty(value = "EventCode") 22 | private final String eventCode; 23 | @JsonProperty(value = "TerminationReason") 24 | private final String terminationReason; 25 | 26 | /** 27 | * Constructor for NotifyServerProcessTerminationRequest 28 | * @param processId 29 | * @param eventCode 30 | * @param terminationReason 31 | */ 32 | public NotifyServerProcessTerminationRequest(final String processId, 33 | final String eventCode, 34 | final String terminationReason) { 35 | this.processId = processId; 36 | this.eventCode = eventCode; 37 | this.terminationReason = terminationReason; 38 | setAction(WebSocketActions.NotifyServerProcessTermination.name()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/SendHeartbeatRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket; 5 | 6 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketRequest; 7 | import com.amazon.gamelift.agent.model.constants.WebSocketActions; 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | import lombok.Builder; 10 | import lombok.Data; 11 | import lombok.EqualsAndHashCode; 12 | import lombok.NonNull; 13 | import lombok.ToString; 14 | 15 | import java.util.List; 16 | 17 | @Data 18 | @Builder 19 | @ToString(callSuper = true) 20 | @EqualsAndHashCode(callSuper = true) 21 | public class SendHeartbeatRequest extends WebsocketRequest { 22 | @JsonProperty(value = "Status") 23 | @NonNull 24 | private final String status; 25 | @JsonProperty(value = "ProcessList") 26 | @NonNull 27 | private final List processList; 28 | @JsonProperty(value = "HeartbeatTimeMillis") 29 | private final long heartbeatTimeMillis; 30 | 31 | /** 32 | * Constructor for SendHeartbeatRequest 33 | * @param status 34 | * @param processList 35 | * @param heartbeatTimeMillis 36 | */ 37 | public SendHeartbeatRequest(final @NonNull String status, 38 | final @NonNull List processList, 39 | final long heartbeatTimeMillis) { 40 | this.status = status; 41 | this.processList = processList; 42 | this.heartbeatTimeMillis = heartbeatTimeMillis; 43 | setAction(WebSocketActions.SendHeartbeat.name()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/logging/AgentLogFileFilterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.logging; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertFalse; 7 | import static org.mockito.Mockito.when; 8 | 9 | import java.io.File; 10 | 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.mockito.InjectMocks; 14 | import org.mockito.Mock; 15 | import org.mockito.junit.jupiter.MockitoExtension; 16 | 17 | @ExtendWith(MockitoExtension.class) 18 | public class AgentLogFileFilterTest { 19 | 20 | @Mock private File testFile; 21 | 22 | @InjectMocks private AgentLogFileFilter filter; 23 | 24 | @Test 25 | public void GIVEN_notAFile_WHEN_accept_THEN_returnsFalse() { 26 | when(testFile.getName()).thenReturn("gameliftagent.log"); 27 | when(testFile.isFile()).thenReturn(false); 28 | assertFalse(filter.accept(testFile)); 29 | } 30 | 31 | @Test 32 | public void GIVEN_badFileName_WHEN_accept_THEN_returnsFalse() { 33 | when(testFile.isFile()).thenReturn(true); 34 | when(testFile.isDirectory()).thenReturn(false); 35 | when(testFile.getName()).thenReturn("somelogfile.log"); 36 | assertFalse(filter.accept(testFile)); 37 | } 38 | 39 | @Test 40 | public void GIVEN_directory_WHEN_accept_THEN_returnsFalse() { 41 | when(testFile.isFile()).thenReturn(true); 42 | when(testFile.isDirectory()).thenReturn(true); 43 | when(testFile.getName()).thenReturn("gameliftagent.log"); 44 | assertFalse(filter.accept(testFile)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/module/CliModule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.module; 5 | 6 | import com.fasterxml.jackson.core.JsonGenerator; 7 | import com.fasterxml.jackson.databind.DeserializationFeature; 8 | import com.fasterxml.jackson.databind.MapperFeature; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 11 | import dagger.Module; 12 | import dagger.Provides; 13 | import org.apache.commons.cli.CommandLineParser; 14 | import org.apache.commons.cli.DefaultParser; 15 | import org.apache.commons.cli.HelpFormatter; 16 | 17 | /** 18 | * Module to provide the dependencies for the cli parser. 19 | */ 20 | @Module 21 | public class CliModule { 22 | /** 23 | * Provides CommandLineParser 24 | * @return 25 | */ 26 | @Provides 27 | public CommandLineParser provideCommandLineParser() { 28 | return new DefaultParser(); 29 | } 30 | 31 | /** 32 | * Provides HelpFormatter 33 | * @return 34 | */ 35 | @Provides 36 | public HelpFormatter provideHelpFormatter() { 37 | return new HelpFormatter(); 38 | } 39 | 40 | /** 41 | * Provides ObjectMapper 42 | * @return 43 | */ 44 | @Provides 45 | public ObjectMapper provideObjectMapper() { 46 | return new ObjectMapper() 47 | .configure(JsonGenerator.Feature.IGNORE_UNKNOWN, true) 48 | .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) 49 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 50 | .registerModule(new JavaTimeModule()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/websocket/handlers/MessageHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.websocket.handlers; 5 | 6 | import com.amazon.gamelift.agent.model.exception.MalformedRequestException; 7 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketMessage; 8 | import com.fasterxml.jackson.core.JsonProcessingException; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | /** 13 | * Interface class for handlers for messages received over the web socket 14 | */ 15 | @Slf4j 16 | public abstract class MessageHandler { 17 | private final ObjectMapper objectMapper; 18 | private final Class clazz; 19 | 20 | /** 21 | * Constructor for MessageHandler 22 | * @param clazz 23 | * @param objectMapper 24 | */ 25 | public MessageHandler(final Class clazz, final ObjectMapper objectMapper) { 26 | this.clazz = clazz; 27 | this.objectMapper = objectMapper; 28 | } 29 | 30 | /** 31 | * Handle a message (as string) 32 | * @param message 33 | * @throws MalformedRequestException 34 | */ 35 | public void handle(final String message) throws MalformedRequestException { 36 | try { 37 | handle(objectMapper.readValue(message, clazz)); 38 | } catch (final JsonProcessingException e) { 39 | log.error("Failed to parse Websocket message: {}", message, e); 40 | throw new MalformedRequestException(String.format("Could not parse message %s", message), e); 41 | } 42 | } 43 | 44 | /** 45 | * Handle message 46 | * @param message 47 | */ 48 | public abstract void handle(T message); 49 | } 50 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/websocket/GetFleetRoleCredentialsResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket; 5 | 6 | import com.amazon.gamelift.agent.model.websocket.base.WebsocketResponse; 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | import lombok.Builder; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.Value; 11 | import lombok.extern.jackson.Jacksonized; 12 | 13 | /** 14 | * Response for GetFleetRoleCredentialsResponse. This is the model for the immediate json output from the websocket. 15 | * Other classes should use FleetRoleCredentialsConfiguration (pared-down model) 16 | */ 17 | @Builder 18 | @Jacksonized 19 | @Value 20 | @EqualsAndHashCode(callSuper = true) 21 | public class GetFleetRoleCredentialsResponse extends WebsocketResponse { 22 | @JsonProperty("AssumedRoleUserArn") 23 | private String assumedRoleUserArn; 24 | @JsonProperty("AssumedRoleId") 25 | private String assumedRoleId; 26 | @JsonProperty("AccessKeyId") 27 | private String accessKeyId; 28 | @JsonProperty("SecretAccessKey") 29 | private String secretAccessKey; 30 | @JsonProperty("SessionToken") 31 | private String sessionToken; 32 | @JsonProperty("Expiration") 33 | private Long expiration; 34 | 35 | /** 36 | * Custom toString for GetFleetRoleCredentialsResponse 37 | * @return 38 | */ 39 | @Override 40 | public String toString() { 41 | final String baseString = super.toString(); 42 | return String.format(baseString + " AssumedRoleUserArn: [%s] AssumedRoleId: [%s] Expiration: [%s]", 43 | getAssumedRoleUserArn(), 44 | getAssumedRoleId(), 45 | getExpiration()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/websocket/handlers/NotifyGameSessionActivatedHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.websocket.handlers; 5 | 6 | import com.amazon.gamelift.agent.model.exception.NotFoundException; 7 | import com.amazon.gamelift.agent.model.websocket.NotifyGameSessionActivatedMessage; 8 | import com.amazon.gamelift.agent.process.GameProcessManager; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | import javax.inject.Inject; 13 | 14 | @Slf4j 15 | public class NotifyGameSessionActivatedHandler extends MessageHandler { 16 | 17 | private final GameProcessManager gameProcessManager; 18 | 19 | /** 20 | * Constructor for NotifyGameSessionActivatedHandler 21 | * See GameLiftAgentWebSocketListener for message routing by Action 22 | * @param objectMapper 23 | */ 24 | @Inject 25 | public NotifyGameSessionActivatedHandler(final ObjectMapper objectMapper, 26 | final GameProcessManager gameProcessManager) { 27 | super(NotifyGameSessionActivatedMessage.class, objectMapper); 28 | this.gameProcessManager = gameProcessManager; 29 | } 30 | 31 | @Override 32 | public void handle(final NotifyGameSessionActivatedMessage message) { 33 | log.info("NotifyGameSessionActivatedMessage message received: Process {}", message.getProcessId()); 34 | 35 | try { 36 | gameProcessManager.updateProcessOnGameSessionActivation(message.getProcessId(), message.getGameSessionId()); 37 | } catch (final NotFoundException nfe) { 38 | log.info("Swallowing NotFoundException when saving log paths: {}", nfe.getMessage()); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/ProcessTerminationReason.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model; 5 | 6 | import com.amazonaws.services.gamelift.model.EventCode; 7 | import lombok.RequiredArgsConstructor; 8 | 9 | /** 10 | * Representation of the reason why a Game Process gets terminated. 11 | * Each reason may have an associate EventCode from the GameLift SDK model, which will be reported when sending 12 | * a notification about the process termination in order to create an associated Fleet Event. 13 | */ 14 | @RequiredArgsConstructor 15 | public enum ProcessTerminationReason { 16 | SERVER_PROCESS_CRASHED(EventCode.SERVER_PROCESS_CRASHED), 17 | SERVER_PROCESS_FORCE_TERMINATED(EventCode.SERVER_PROCESS_FORCE_TERMINATED), 18 | SERVER_PROCESS_INVALID_PATH(EventCode.SERVER_PROCESS_INVALID_PATH), 19 | SERVER_PROCESS_PROCESS_EXIT_TIMEOUT(EventCode.SERVER_PROCESS_PROCESS_EXIT_TIMEOUT), 20 | SERVER_PROCESS_PROCESS_READY_TIMEOUT(EventCode.SERVER_PROCESS_PROCESS_READY_TIMEOUT), 21 | SERVER_PROCESS_SDK_INITIALIZATION_TIMEOUT(EventCode.SERVER_PROCESS_SDK_INITIALIZATION_TIMEOUT), 22 | SERVER_PROCESS_TERMINATED_UNHEALTHY(EventCode.SERVER_PROCESS_TERMINATED_UNHEALTHY), 23 | COMPUTE_SHUTTING_DOWN(null), 24 | CUSTOMER_INITIATED(null), 25 | NORMAL_TERMINATION(null); 26 | 27 | private final EventCode eventCode; 28 | 29 | /** 30 | * Retrieves a stringified version of the associated GameLift event code to send for the termination, 31 | * or returns null if there's no associated event that needs to be reported for the termination reason. 32 | * 33 | * @return the associated event code string, or null if no event code is specified 34 | */ 35 | public String getEventCode() { 36 | return eventCode == null ? null : eventCode.toString(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/websocket/handlers/RefreshConnectionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.websocket.handlers; 5 | 6 | import com.amazon.gamelift.agent.model.websocket.RefreshConnectionMessage; 7 | import com.amazon.gamelift.agent.websocket.WebSocketConnectionManager; 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | import dagger.Lazy; 10 | import lombok.extern.slf4j.Slf4j; 11 | import javax.inject.Inject; 12 | 13 | @Slf4j 14 | public class RefreshConnectionHandler extends MessageHandler { 15 | public static final String ACTION = "RefreshConnection"; 16 | 17 | // Why Lazy? WebSocketConnectionManager depends on Map>, and 18 | // Map> depends on this class. To get around this, Dagger allows 19 | // Lazy initialization of a field, so it works, as long as it is not accessed in the constructor. 20 | private final Lazy connectionManager; 21 | 22 | /** 23 | * Constructor for RefreshConnectionHandler 24 | * See GameLiftAgentWebSocketListener for message routing by Action 25 | * @param connectionManager 26 | * @param objectMapper 27 | */ 28 | @Inject 29 | public RefreshConnectionHandler(final Lazy connectionManager, 30 | final ObjectMapper objectMapper) { 31 | super(RefreshConnectionMessage.class, objectMapper); 32 | this.connectionManager = connectionManager; 33 | } 34 | 35 | @Override 36 | public void handle(RefreshConnectionMessage message) { 37 | try { 38 | connectionManager.get().refreshWebSocketConnection(message); 39 | } catch (final Exception e) { 40 | log.error("Failed to process refresh connection message. Will retry on the next heartbeat", e); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/websocket/handlers/ForceExitProcessHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.websocket.handlers; 5 | 6 | import com.amazon.gamelift.agent.model.ProcessTerminationReason; 7 | import com.amazon.gamelift.agent.model.websocket.ForceExitProcessMessage; 8 | import com.amazon.gamelift.agent.model.constants.WebSocketActions; 9 | import com.amazon.gamelift.agent.process.GameProcessManager; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | import lombok.extern.slf4j.Slf4j; 12 | 13 | import javax.inject.Inject; 14 | 15 | @Slf4j 16 | @Deprecated 17 | public class ForceExitProcessHandler extends MessageHandler { 18 | 19 | private final GameProcessManager gameProcessManager; 20 | 21 | /** 22 | * Constructor for ForceExitProcessHandler 23 | * See GameLiftAgentWebSocketListener for message routing by Action 24 | * @param objectMapper 25 | * @param gameProcessManager 26 | */ 27 | @Inject 28 | public ForceExitProcessHandler(final ObjectMapper objectMapper, 29 | final GameProcessManager gameProcessManager) { 30 | super(ForceExitProcessMessage.class, objectMapper); 31 | this.gameProcessManager = gameProcessManager; 32 | } 33 | 34 | @Override 35 | public void handle(final ForceExitProcessMessage message) { 36 | log.info("Forcing process to exit message received: {}", message); 37 | 38 | if (!WebSocketActions.ForceExitProcess.name().equals(message.getAction())) { 39 | log.error("Message is not a ForceExitProcess action. Is {}. Not processing", 40 | message.getAction()); 41 | return; 42 | } 43 | 44 | log.info("Force exiting process with UUID: {}", message.getProcessId()); 45 | gameProcessManager.terminateProcessByUUID(message.getProcessId(), 46 | ProcessTerminationReason.NORMAL_TERMINATION); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/websocket/handlers/NotifyProcessRegisteredHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.websocket.handlers; 5 | 6 | import com.amazon.gamelift.agent.model.exception.NotFoundException; 7 | import com.amazon.gamelift.agent.model.websocket.NotifyProcessRegisteredMessage; 8 | import com.amazon.gamelift.agent.process.GameProcessManager; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.apache.commons.collections4.CollectionUtils; 12 | 13 | import javax.inject.Inject; 14 | 15 | @Slf4j 16 | public class NotifyProcessRegisteredHandler extends MessageHandler { 17 | 18 | private final GameProcessManager gameProcessManager; 19 | 20 | /** 21 | * Constructor for NotifyProcessRegisteredHandler 22 | * See GameLiftAgentWebSocketListener for message routing by Action 23 | * @param objectMapper 24 | */ 25 | @Inject 26 | public NotifyProcessRegisteredHandler(final ObjectMapper objectMapper, 27 | final GameProcessManager gameProcessManager) { 28 | super(NotifyProcessRegisteredMessage.class, objectMapper); 29 | this.gameProcessManager = gameProcessManager; 30 | } 31 | 32 | @Override 33 | public void handle(final NotifyProcessRegisteredMessage message) { 34 | log.info("NotifyProcessRegistered message received: Process {}", message.getProcessId()); 35 | 36 | try { 37 | if (CollectionUtils.isNotEmpty(message.getLogPaths())) { 38 | log.info("Recording log paths for process {}: {}", message.getProcessId(), message.getLogPaths()); 39 | gameProcessManager.updateProcessOnRegistration(message.getProcessId(), message.getLogPaths()); 40 | } 41 | } catch (final NotFoundException nfe) { 42 | log.info("Swallowing NotFoundException when saving log paths: {}", nfe.getMessage()); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/process/destroyer/ProcessDestroyerFactoryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.process.destroyer; 5 | 6 | import com.amazon.gamelift.agent.model.OperatingSystem; 7 | 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.junit.jupiter.params.ParameterizedTest; 11 | import org.junit.jupiter.params.provider.ValueSource; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | import static org.junit.jupiter.api.Assertions.assertInstanceOf; 14 | import static org.junit.jupiter.api.Assertions.assertThrows; 15 | 16 | @ExtendWith(MockitoExtension.class) 17 | public class ProcessDestroyerFactoryTest { 18 | @ParameterizedTest 19 | @ValueSource(strings = {"WIN_2012", "WINDOWS_2016", "WINDOWS_2019", "WINDOWS_2022", "UNKNOWN_WINDOWS"}) 20 | public void GIVEN_windowsOs_WHEN_getProcessDestroyer_THEN_returnsWindowsProcessDestroyer(String os) { 21 | // GIVEN 22 | OperatingSystem operatingSystem = OperatingSystem.fromString(os); 23 | // WHEN 24 | ProcessDestroyer processDestroyer = ProcessDestroyerFactory.getProcessDestroyer(operatingSystem); 25 | // THEN 26 | assertInstanceOf(WindowsProcessDestroyer.class, processDestroyer); 27 | } 28 | 29 | @ParameterizedTest 30 | @ValueSource(strings = {"AMAZON_LINUX_2", "AMAZON_LINUX_2023", "UNKNOWN_LINUX"}) 31 | public void GIVEN_linuxOs_WHEN_getProcessDestroyer_THEN_returnsLinuxProcessDestroyer(String os) { 32 | // GIVEN 33 | OperatingSystem operatingSystem = OperatingSystem.fromString(os); 34 | // WHEN 35 | ProcessDestroyer processDestroyer = ProcessDestroyerFactory.getProcessDestroyer(operatingSystem); 36 | // THEN 37 | assertInstanceOf(LinuxProcessDestroyer.class, processDestroyer); 38 | } 39 | 40 | @Test 41 | public void GIVEN_unsupportedOs_WHEN_getProcessDestroyer_THEN_throwsException() { 42 | // GIVEN 43 | OperatingSystem operatingSystem = OperatingSystem.INVALID; 44 | // WHEN 45 | assertThrows(IllegalArgumentException.class, () -> ProcessDestroyerFactory.getProcessDestroyer(operatingSystem)); 46 | // THEN - Exception 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/cache/RuntimeConfigurationCacheLoaderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.cache; 5 | 6 | import com.amazon.gamelift.agent.model.exception.NotFoundException; 7 | import com.amazon.gamelift.agent.model.exception.AgentException; 8 | import com.amazon.gamelift.agent.websocket.AgentWebSocket; 9 | import com.amazon.gamelift.agent.websocket.WebSocketConnectionProvider; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.mockito.InjectMocks; 14 | import org.mockito.Mock; 15 | import org.mockito.junit.jupiter.MockitoExtension; 16 | 17 | import java.util.concurrent.TimeoutException; 18 | 19 | import static org.junit.jupiter.api.Assertions.assertThrows; 20 | import static org.mockito.ArgumentMatchers.any; 21 | import static org.mockito.BDDMockito.given; 22 | import static org.mockito.Mockito.mock; 23 | import static org.mockito.Mockito.when; 24 | 25 | @ExtendWith(MockitoExtension.class) 26 | public class RuntimeConfigurationCacheLoaderTest { 27 | 28 | private static final String RUNTIME_CONFIG_CACHE_KEY = "runtimeConfiguration"; 29 | @Mock 30 | private AgentWebSocket client; 31 | @Mock 32 | private WebSocketConnectionProvider webSocketConnectionProvider; 33 | @InjectMocks 34 | private RuntimeConfigCacheLoader cacheLoader; 35 | 36 | @BeforeEach 37 | public void setup() { 38 | when(webSocketConnectionProvider.getCurrentConnection()).thenReturn(client); 39 | } 40 | 41 | @Test 42 | public void GIVEN_nullConfig_WHEN_load_THEN_exception() throws AgentException { 43 | when(client.sendRequest(any(), any(), any())).thenReturn(null); 44 | 45 | assertThrows(NotFoundException.class, () -> 46 | cacheLoader.load(RUNTIME_CONFIG_CACHE_KEY)); 47 | } 48 | 49 | @Test 50 | public void GIVEN_exception_WHEN_load_THEN_throwException() throws AgentException { 51 | given(client.sendRequest(any(), any(), any())).willAnswer( invocation -> { throw mock(TimeoutException.class); }); 52 | 53 | assertThrows(TimeoutException.class, () -> 54 | cacheLoader.load(RUNTIME_CONFIG_CACHE_KEY)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/process/destroyer/WindowsProcessDestroyerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.process.destroyer; 5 | 6 | import com.amazon.gamelift.agent.model.OperatingSystem; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.InOrder; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | 14 | import java.util.stream.Stream; 15 | 16 | import static org.mockito.Mockito.inOrder; 17 | import static org.mockito.Mockito.when; 18 | 19 | @ExtendWith(MockitoExtension.class) 20 | public class WindowsProcessDestroyerTest { 21 | 22 | private static final OperatingSystem OPERATING_SYSTEM = OperatingSystem.WINDOWS_2019; 23 | 24 | @Mock private Process mockInternalProcess; 25 | @Mock private ProcessHandle mockChildProcessHandle; 26 | 27 | private WindowsProcessDestroyer windowsProcessDestroyer; 28 | 29 | @BeforeEach 30 | public void setup() { 31 | windowsProcessDestroyer = new WindowsProcessDestroyer(OPERATING_SYSTEM); 32 | } 33 | 34 | @Test 35 | public void GIVEN_validProcess_WHEN_destroyProcess_THEN_killProcessAndSubProcesses() { 36 | // GIVEN 37 | when(mockInternalProcess.descendants()).thenReturn(Stream.of(mockChildProcessHandle)); 38 | 39 | // WHEN 40 | windowsProcessDestroyer.destroyProcess(mockInternalProcess); 41 | 42 | // THEN 43 | // Verify the child process is destroyed first 44 | InOrder inOrder = inOrder(mockChildProcessHandle, mockInternalProcess); 45 | inOrder.verify(mockChildProcessHandle).destroyForcibly(); 46 | inOrder.verify(mockInternalProcess).destroyForcibly(); 47 | } 48 | 49 | @Test 50 | public void GIVEN_destroyForciblyFails_WHEN_destroyProcess_THEN_nothing() { 51 | // GIVEN 52 | when(mockInternalProcess.descendants()).thenReturn(Stream.of(mockChildProcessHandle)); 53 | when(mockInternalProcess.destroyForcibly()).thenThrow(new IllegalStateException()); 54 | 55 | // WHEN 56 | windowsProcessDestroyer.destroyProcess(mockInternalProcess); 57 | 58 | // THEN - Nothing 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/logging/GameSessionLogsErrorReadMeFile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.logging; 5 | 6 | import lombok.extern.slf4j.Slf4j; 7 | 8 | import java.io.BufferedWriter; 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.nio.file.Files; 12 | import java.nio.file.StandardOpenOption; 13 | 14 | import static java.nio.charset.StandardCharsets.UTF_8; 15 | 16 | /** 17 | * Encapsulate access to the log error ReadMe file generated when collecting GameServerLogs. 18 | */ 19 | @Slf4j 20 | public class GameSessionLogsErrorReadMeFile { 21 | private BufferedWriter logErrorReadMeFile; 22 | private boolean isOpen = false; 23 | 24 | /** 25 | * Constructor for GameSessionLogsErrorReadMeFile 26 | * @param directory 27 | * @param fileName 28 | */ 29 | public GameSessionLogsErrorReadMeFile(final File directory, final String fileName) { 30 | try { 31 | logErrorReadMeFile = Files.newBufferedWriter(new File(directory, fileName).toPath(), 32 | UTF_8, StandardOpenOption.CREATE_NEW); 33 | isOpen = true; 34 | writeLine("Game Server Logs"); 35 | } catch (final IOException e) { 36 | log.error("Could not open readMe file for writing", e); 37 | } 38 | 39 | } 40 | 41 | /** 42 | * Writes a string to the ErrorReadeFile 43 | * @param s 44 | */ 45 | public void writeLine(final String s) { 46 | if (!isOpen) { 47 | log.error("Could not write readme, file is not open. Text to Write: {}", s); 48 | return; 49 | } 50 | try { 51 | logErrorReadMeFile.write(s); 52 | logErrorReadMeFile.newLine(); 53 | } catch (final IOException e) { 54 | log.error("Could not write line {}", s, e); 55 | } 56 | 57 | } 58 | 59 | /** 60 | * CLoses ErrorReadMeFile 61 | */ 62 | public void close() { 63 | if (!isOpen) { 64 | return; 65 | } 66 | try { 67 | logErrorReadMeFile.close(); 68 | isOpen = false; 69 | } catch (final IOException e) { 70 | log.error("Could not close file, may lose data", e); 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/utils/AmazonGameLiftRetryCondition.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.utils; 5 | 6 | import com.amazonaws.AmazonClientException; 7 | import com.amazonaws.AmazonWebServiceRequest; 8 | import com.amazonaws.retry.PredefinedRetryPolicies; 9 | import com.amazonaws.services.gamelift.model.ConflictException; 10 | import com.amazonaws.services.gamelift.model.InternalServiceException; 11 | import com.amazonaws.services.gamelift.model.NotFoundException; 12 | import com.amazonaws.services.gamelift.model.NotReadyException; 13 | import lombok.RequiredArgsConstructor; 14 | 15 | @RequiredArgsConstructor 16 | public class AmazonGameLiftRetryCondition extends PredefinedRetryPolicies.SDKDefaultRetryCondition { 17 | 18 | /** 19 | * Override shouldRetry method to add GameLift exceptions. 20 | */ 21 | @Override 22 | public boolean shouldRetry(final AmazonWebServiceRequest originalRequest, 23 | final AmazonClientException exception, 24 | final int retries) { 25 | // Parent class handles IOException, 5XXs, and ClockSkew issues. 26 | // Second method checks for retryable GameLift exceptions. 27 | return super.shouldRetry(originalRequest, exception, retries) || isRetryableGameLiftException(exception); 28 | } 29 | 30 | /** 31 | * Determine whether to retry based on GameLift exceptions. 32 | */ 33 | private boolean isRetryableGameLiftException(final AmazonClientException exception) { 34 | if (exception instanceof ConflictException) { 35 | // Potential conflict could succeed on retries, otherwise will exhaust retries and throw ConflictException 36 | return true; 37 | } else if (exception instanceof InternalServiceException) { 38 | // Unknown service failure, always retry 39 | return true; 40 | } else if (exception instanceof NotFoundException) { 41 | // Resource could be eventually consistent, otherwise will exhaust retries and throw NotFoundException 42 | return true; 43 | } else if (exception instanceof NotReadyException) { 44 | // Resource is not ready yet, always retry 45 | return true; 46 | } 47 | return false; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/cache/ComputeAuthTokenCacheLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.cache; 5 | 6 | import com.amazon.gamelift.agent.model.exception.AgentException; 7 | import com.amazon.gamelift.agent.model.gamelift.GetComputeAuthTokenResponse; 8 | import com.amazon.gamelift.agent.client.AmazonGameLiftClientWrapper; 9 | import com.amazon.gamelift.agent.utils.RetryHelper; 10 | import com.amazonaws.services.gamelift.model.GetComputeAuthTokenRequest; 11 | import com.google.common.cache.CacheLoader; 12 | 13 | import lombok.NonNull; 14 | import lombok.extern.slf4j.Slf4j; 15 | 16 | @Slf4j 17 | public class ComputeAuthTokenCacheLoader extends CacheLoader { 18 | private static final int MAX_GET_COMPUTE_AUTH_TOKEN_RETRIES = 16; 19 | 20 | private final AmazonGameLiftClientWrapper amazonGameLift; 21 | private final String fleetId; 22 | private final String computeName; 23 | 24 | /** 25 | * Constructor for ComputeAuthTokenCacheLoader 26 | * @param amazonGameLift 27 | * @param fleetId 28 | * @param computeName 29 | */ 30 | public ComputeAuthTokenCacheLoader(final AmazonGameLiftClientWrapper amazonGameLift, 31 | final String fleetId, 32 | final String computeName) { 33 | this.amazonGameLift = amazonGameLift; 34 | this.fleetId = fleetId; 35 | this.computeName = computeName; 36 | } 37 | 38 | @Override 39 | public @NonNull GetComputeAuthTokenResponse load(final @NonNull String key) throws RuntimeException { 40 | log.info("Loading ComputeAuthToken into cache via Amazon GameLift GetComputeAuthToken call."); 41 | try { 42 | final GetComputeAuthTokenRequest getComputeAuthTokenRequest = new GetComputeAuthTokenRequest() 43 | .withFleetId(fleetId) 44 | .withComputeName(computeName); 45 | 46 | return RetryHelper.runRetryable(MAX_GET_COMPUTE_AUTH_TOKEN_RETRIES, true, () -> amazonGameLift.getComputeAuthToken(getComputeAuthTokenRequest)); 47 | } catch (final AgentException e) { 48 | log.error("Call to Amazon GameLift GetComputeAuthToken failed.", e); 49 | throw new RuntimeException(e); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/process/destroyer/WindowsProcessDestroyer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.process.destroyer; 5 | 6 | import com.amazon.gamelift.agent.model.OperatingSystem; 7 | import com.amazon.gamelift.agent.model.OperatingSystemFamily; 8 | 9 | import javax.inject.Inject; 10 | import javax.inject.Singleton; 11 | import lombok.extern.slf4j.Slf4j; 12 | 13 | @Slf4j 14 | @Singleton 15 | public class WindowsProcessDestroyer implements ProcessDestroyer { 16 | private final OperatingSystem operatingSystem; 17 | 18 | /** 19 | * Constructor for WindowsProcessDestroyer 20 | * @param operatingSystem 21 | */ 22 | @Inject 23 | public WindowsProcessDestroyer(final OperatingSystem operatingSystem) { 24 | if (!OperatingSystemFamily.WINDOWS.equals(operatingSystem.getOperatingSystemFamily())) { 25 | //Creation validation. This class should only be used for Windows-based OS 26 | throw new IllegalArgumentException("Attempted to create Windows process for non Windows-based OS. Found " 27 | + operatingSystem); 28 | } 29 | this.operatingSystem = operatingSystem; 30 | } 31 | 32 | @Override 33 | public void destroyProcess(final Process internalProcess) { 34 | try { 35 | // Destroy all sub-processes first, but be sure to use .descendents() not .children() because .descendants() 36 | // includes all processes created by the root or by children of the root. 37 | // children() only includes direct children of the root process. 38 | // This should be done before destroying the root process to avoid race conditions with the OS. 39 | internalProcess.descendants().forEach(processHandle -> { 40 | log.info("Destroying sub-process with PID {}", processHandle.pid()); 41 | processHandle.destroyForcibly(); 42 | }); 43 | 44 | // Destroy the root process. This should be the "powershell" process on Windows. 45 | log.info("Destroying parent process with PID {}", internalProcess.pid()); 46 | internalProcess.destroyForcibly(); 47 | } catch (final Exception e) { 48 | log.error("Failure terminating process: {}", internalProcess.pid()); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/utils/EnvironmentHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.utils; 5 | 6 | import com.amazon.gamelift.agent.model.constants.EnvironmentConstants; 7 | import com.amazonaws.AmazonClientException; 8 | import com.amazonaws.util.EC2MetadataUtils; 9 | import com.google.common.collect.ImmutableMap; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | import java.util.Map; 13 | 14 | @Slf4j 15 | public class EnvironmentHelper { 16 | private static Map getHostInfoFromEC2Metadata() throws AmazonClientException { 17 | return ImmutableMap.of( 18 | EnvironmentConstants.HOST_ID_KEY, EC2MetadataUtils.getInstanceId(), 19 | // Network metadata requires a mac address to use helper methods. Using getData for default mac resolution 20 | EnvironmentConstants.HOST_NAME_KEY, EC2MetadataUtils.getData(EnvironmentConstants.INSTANCE_HOSTNAME_KEY), 21 | EnvironmentConstants.HOST_PUBLICIPV4_KEY, EC2MetadataUtils.getData(EnvironmentConstants.INSTANCE_PUBLICIPV4_KEY), 22 | EnvironmentConstants.HOST_INSTANCE_TYPE_KEY, EC2MetadataUtils.getInstanceType(), 23 | EnvironmentConstants.AMI_ID, EC2MetadataUtils.getAmiId()); 24 | } 25 | 26 | /** 27 | * Utility method to log the current EC2Metadata 28 | */ 29 | public static void logEC2Metadata() { 30 | try { 31 | final Map hostInfo = getHostInfoFromEC2Metadata(); 32 | log.info("EC2 metadata: " 33 | + "instanceId: {}, " 34 | + "instanceType: {}, " 35 | + "PublicIpAddress: {}, " 36 | + "DnsName (hostName): {}, " 37 | + "AMI ID: {}", 38 | hostInfo.get(EnvironmentConstants.HOST_ID_KEY), 39 | hostInfo.get(EnvironmentConstants.HOST_INSTANCE_TYPE_KEY), 40 | hostInfo.get(EnvironmentConstants.HOST_PUBLICIPV4_KEY), 41 | hostInfo.get(EnvironmentConstants.HOST_NAME_KEY), 42 | hostInfo.get(EnvironmentConstants.AMI_ID) 43 | ); 44 | } catch (final Exception e) { 45 | log.warn("EC2 metadata could not be logged. ", e); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/process/builder/WindowsProcessHandle.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.process.builder; 5 | 6 | import com.sun.jna.platform.win32.Kernel32; 7 | import com.sun.jna.platform.win32.WinDef; 8 | import com.sun.jna.platform.win32.WinNT; 9 | import lombok.RequiredArgsConstructor; 10 | import org.apache.commons.lang3.NotImplementedException; 11 | 12 | import java.util.Optional; 13 | import java.util.concurrent.CompletableFuture; 14 | import java.util.stream.Stream; 15 | 16 | @RequiredArgsConstructor 17 | public class WindowsProcessHandle implements ProcessHandle { 18 | private final WinDef.DWORD processId; 19 | private final WinNT.HANDLE processHandle; 20 | 21 | @Override 22 | public long pid() { 23 | return processId.longValue(); 24 | } 25 | 26 | @Override 27 | public Optional parent() { 28 | throw new NotImplementedException("Not implemented"); 29 | } 30 | 31 | @Override 32 | public Stream children() { 33 | return WindowsProcessCommons.getChildren(processId.longValue()); 34 | } 35 | 36 | @Override 37 | public Stream descendants() { 38 | throw new NotImplementedException("Not implemented"); 39 | } 40 | 41 | @Override 42 | public Info info() { 43 | throw new NotImplementedException("Not implemented"); 44 | } 45 | 46 | @Override 47 | public CompletableFuture onExit() { 48 | throw new NotImplementedException("Not implemented"); 49 | } 50 | 51 | @Override 52 | public boolean supportsNormalTermination() { 53 | throw new NotImplementedException("Not implemented"); 54 | } 55 | 56 | @Override 57 | public boolean destroy() { 58 | throw new NotImplementedException("Not implemented"); 59 | } 60 | 61 | @Override 62 | public boolean destroyForcibly() { 63 | return Kernel32.INSTANCE.TerminateProcess(processHandle, WindowsProcessCommons.FORCE_TERMINATED_EXIT_CODE); 64 | } 65 | 66 | @Override 67 | public boolean isAlive() { 68 | throw new NotImplementedException("Not implemented"); 69 | } 70 | 71 | @Override 72 | public int compareTo(ProcessHandle other) { 73 | throw new NotImplementedException("Not implemented"); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/manager/DynamicRuntimeConfigurationManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.manager; 5 | 6 | import com.amazon.gamelift.agent.cache.RuntimeConfigCacheLoader; 7 | import com.amazon.gamelift.agent.model.RuntimeConfiguration; 8 | import com.amazon.gamelift.agent.websocket.WebSocketConnectionProvider; 9 | import com.google.common.cache.CacheBuilder; 10 | import com.google.common.cache.LoadingCache; 11 | import lombok.extern.slf4j.Slf4j; 12 | 13 | import java.time.Duration; 14 | import java.util.concurrent.ExecutionException; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | /** 18 | * This instance of the RuntimeConfigurationManager obtains runtime config from Amazon GameLift over the 19 | * websocket connection. It is periodically refreshed in the background to be able to pick 20 | * up changes to process count, etc. on the fly. 21 | */ 22 | @Slf4j 23 | public class DynamicRuntimeConfigurationManager implements RuntimeConfigurationManager { 24 | private final LoadingCache runtimeConfigCache; 25 | private static final String RUNTIME_CONFIG_CACHE_KEY = "runtimeConfiguration"; 26 | private static final int MAX_CACHE_ENTRIES = 1; 27 | private static final Duration CACHE_EXPIRATION_TIME = Duration.ofMinutes(5); 28 | 29 | /** 30 | * Constructor for DynamicRuntimeConfigurationManager 31 | * @param webSocketConnectionProvider 32 | */ 33 | public DynamicRuntimeConfigurationManager(final WebSocketConnectionProvider webSocketConnectionProvider) { 34 | this.runtimeConfigCache = CacheBuilder.newBuilder() 35 | .maximumSize(MAX_CACHE_ENTRIES) 36 | .expireAfterWrite(CACHE_EXPIRATION_TIME.toMillis(), TimeUnit.MILLISECONDS) 37 | .recordStats() 38 | .build(new RuntimeConfigCacheLoader(webSocketConnectionProvider)); 39 | } 40 | 41 | @Override 42 | public synchronized RuntimeConfiguration getRuntimeConfiguration() { 43 | try { 44 | return runtimeConfigCache.get(RUNTIME_CONFIG_CACHE_KEY); 45 | } catch (final ExecutionException e) { 46 | log.error("Caught an exception while loading runtimeConfiguration from cache", e); 47 | throw new RuntimeException("Caught an exception while loading runtimeConfiguration from cache", e); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/websocket/handlers/RefreshConnectionHandlerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.websocket.handlers; 5 | 6 | import com.amazon.gamelift.agent.model.exception.InternalServiceException; 7 | import com.amazon.gamelift.agent.model.websocket.RefreshConnectionMessage; 8 | import com.amazon.gamelift.agent.websocket.WebSocketConnectionManager; 9 | 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.mockito.InjectMocks; 14 | import org.mockito.Mock; 15 | import org.mockito.junit.jupiter.MockitoExtension; 16 | import static org.mockito.Mockito.doThrow; 17 | import static org.mockito.Mockito.verify; 18 | import static org.mockito.Mockito.when; 19 | 20 | import dagger.Lazy; 21 | 22 | @ExtendWith(MockitoExtension.class) 23 | public class RefreshConnectionHandlerTest { 24 | private static final String REFRESH_WEB_SOCKET_ENDPOINT = "endpoint"; 25 | private static final String REFRESH_WEB_SOCKET_AUTH_TOKEN = "newAuthToken"; 26 | 27 | @Mock 28 | private WebSocketConnectionManager connectionManager; 29 | @Mock 30 | private Lazy lazyConnectionManager; 31 | 32 | @InjectMocks 33 | private RefreshConnectionHandler connectionHandler; 34 | 35 | private final RefreshConnectionMessage input = new RefreshConnectionMessage(); 36 | 37 | @BeforeEach 38 | public void setup() { 39 | when(lazyConnectionManager.get()).thenReturn(connectionManager); 40 | input.setAuthToken(REFRESH_WEB_SOCKET_AUTH_TOKEN); 41 | input.setRefreshConnectionEndpoint(REFRESH_WEB_SOCKET_ENDPOINT); 42 | } 43 | 44 | @Test 45 | public void GIVEN_input_WHEN_handle_THEN_refreshConnection() throws InternalServiceException { 46 | // WHEN 47 | connectionHandler.handle(input); 48 | 49 | // THEN 50 | verify(connectionManager).refreshWebSocketConnection(input); 51 | } 52 | 53 | @Test 54 | public void GIVEN_input_WHEN_handleFails_THEN_doesntThrow() throws InternalServiceException { 55 | // GIVEN 56 | doThrow(RuntimeException.class).when(connectionManager).refreshWebSocketConnection(input); 57 | 58 | // WHEN 59 | connectionHandler.handle(input); 60 | 61 | // THEN 62 | verify(connectionManager).refreshWebSocketConnection(input); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /install.bat: -------------------------------------------------------------------------------- 1 | : ' Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. ' 2 | 3 | @echo off 4 | 5 | java -version >nul 2>&1 6 | IF %ERRORLEVEL% NEQ 0 ( 7 | echo No Java installed 8 | echo Installing Amazon Corretto 17 9 | curl -LJO https://corretto.aws/downloads/latest/amazon-corretto-17-x64-windows-jdk.zip 10 | powershell -Command "Expand-Archive -Path amazon-corretto-17-x64-windows-jdk.zip -DestinationPath 'C:\Program Files\Amazon Corretto'" 11 | set "JAVA_HOME=C:\Program Files\Amazon Corretto\jdk17.0.10_7" 12 | ) 13 | 14 | for /f "tokens=3" %%g in ('java -version 2^>^&1 ^| findstr /i "version"') do ( 15 | set JVER=%%g 16 | ) 17 | set JVER=%JVER:"=% 18 | 19 | IF "%JVER%" LSS "17.0" ( 20 | echo Java version is too low (%JVER%) 21 | echo At least 17 is needed 22 | echo Installing Amazon Corretto 17 23 | curl -LJO https://corretto.aws/downloads/latest/amazon-corretto-17-x64-windows-jdk.zip 24 | powershell -Command "Expand-Archive -Path amazon-corretto-17-x64-windows-jdk.zip -DestinationPath 'C:\Program Files\Amazon Corretto'" 25 | set "JAVA_HOME=C:\Program Files\Amazon Corretto\jdk17.0.10_7" 26 | ) 27 | 28 | mvn -version 1>nul 2>nul 29 | IF %ERRORLEVEL% NEQ 0 ( 30 | echo No Maven installed 31 | echo Installing Apache Maven 3.9.6 32 | curl -LJO https://dlcdn.apache.org/maven/maven-3/3.9.6/binaries/apache-maven-3.9.6-bin.zip 33 | powershell -Command "Expand-Archive -Path apache-maven-3.9.6-bin.zip -DestinationPath 'C:\Program Files\Apache Maven'" 34 | set "MAVEN_HOME=C:\Program Files\Apache Maven\apache-maven-3.9.6" 35 | setx /M PATH "%PATH%;%MAVEN_HOME%\bin" 36 | ) ELSE ( 37 | for /f "tokens=3" %%g in ('mvn -version 2^>^&1 ^| findstr /i "version"') do ( 38 | set MVER=%%g 39 | ) 40 | set MVER=%MVER:"=% 41 | 42 | IF "%MVER%" LSS "3.2.5" ( 43 | echo Maven version is too low (%MVER%) 44 | echo Installing Apache Maven 3.9.6 45 | curl -LJO https://dlcdn.apache.org/maven/maven-3/3.9.6/binaries/apache-maven-3.9.6-bin.zip 46 | powershell -Command "Expand-Archive -Path apache-maven-3.9.6-bin.zip -DestinationPath 'C:\Program Files\Apache Maven'" 47 | set "MAVEN_HOME=C:\Program Files\Apache Maven\apache-maven-3.9.6" 48 | setx /M PATH "%PATH%;%MAVEN_HOME%\bin" 49 | ) 50 | ) 51 | 52 | set "agent=https://github.com/[ACCOUNT]/[REPOSITORY]/archive/[BRANCH].zip" 53 | echo downloading Gamelift Agent from %agent% 54 | curl -LJO %agent% 55 | tar -xf [REPOSITORY]-[BRANCH].zip 56 | 57 | cd [REPOSITORY]-[BRANCH] 58 | mvn clean compile assembly:single -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/websocket/WebSocketExceptionProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.websocket; 5 | 6 | import com.amazon.gamelift.agent.model.exception.NotReadyException; 7 | import com.amazon.gamelift.agent.model.exception.ThrottlingException; 8 | import com.amazon.gamelift.agent.model.websocket.base.ErrorWebsocketResponse; 9 | import com.amazon.gamelift.agent.model.exception.InternalServiceException; 10 | import com.amazon.gamelift.agent.model.exception.InvalidRequestException; 11 | import com.amazon.gamelift.agent.model.exception.NotFoundException; 12 | import com.amazon.gamelift.agent.model.exception.AgentException; 13 | import com.amazon.gamelift.agent.model.exception.UnauthorizedException; 14 | import com.fasterxml.jackson.core.JsonProcessingException; 15 | import com.fasterxml.jackson.databind.ObjectMapper; 16 | import javax.inject.Singleton; 17 | import lombok.RequiredArgsConstructor; 18 | import org.apache.http.HttpStatus; 19 | 20 | @Singleton 21 | @RequiredArgsConstructor 22 | public class WebSocketExceptionProvider { 23 | 24 | private final ObjectMapper objectMapper; 25 | 26 | /** 27 | * Attempts to deserialize a message from the WebSocket as an error response. 28 | * If the message has an error status code, it'll attempt to translate to a Java exception type. 29 | * Otherwise, it'll return null 30 | */ 31 | public AgentException getExceptionFromWebSocketMessage(final String webSocketMessage) 32 | throws JsonProcessingException { 33 | final ErrorWebsocketResponse testErrorResponse = 34 | objectMapper.readValue(webSocketMessage, ErrorWebsocketResponse.class); 35 | 36 | return switch (testErrorResponse.getStatusCode()) { 37 | case HttpStatus.SC_OK -> null; // If message is not an error response, return null 38 | case HttpStatus.SC_BAD_REQUEST -> new InvalidRequestException(testErrorResponse.getErrorMessage()); 39 | case HttpStatus.SC_UNAUTHORIZED -> new UnauthorizedException(testErrorResponse.getErrorMessage()); 40 | case HttpStatus.SC_NOT_FOUND -> new NotFoundException(testErrorResponse.getErrorMessage()); 41 | case HttpStatus.SC_PRECONDITION_FAILED -> new NotReadyException(testErrorResponse.getErrorMessage()); 42 | case HttpStatus.SC_TOO_MANY_REQUESTS -> new ThrottlingException(testErrorResponse.getErrorMessage()); 43 | default -> new InternalServiceException(testErrorResponse.getErrorMessage()); 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | # 4 | 5 | if type -p java; then 6 | echo found java executable in PATH 7 | _java=java 8 | elif [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then 9 | echo found java executable in JAVA_HOME 10 | _java="$JAVA_HOME/bin/java" 11 | else 12 | echo installing java 17 13 | curl -LJO https://corretto.aws/downloads/latest/amazon-corretto-17-x64-linux-jdk.tar.gz 14 | tar -xvzf amazon-corretto-17-x64-linux-jdk.tar.gz 15 | export JAVA_HOME=$(pwd)/amazon-corretto-17.0.10.8.1-linux-x64 16 | export PATH=$JAVA_HOME/bin:$PATH 17 | fi 18 | 19 | if [[ "$_java" ]]; then 20 | version=$("$_java" -version 2>&1 | awk -F '"' '/version/ {print $2}') 21 | echo version "$version" 22 | if [[ "$version" > "17" ]]; then 23 | echo version is greater than 17, meets requirement 24 | else 25 | echo version is less than 17, needs to install java 17 26 | echo installing java 17 27 | curl -LJO https://corretto.aws/downloads/latest/amazon-corretto-17-x64-linux-jdk.tar.gz 28 | tar -xvzf amazon-corretto-17-x64-linux-jdk.tar.gz 29 | export JAVA_HOME=$(pwd)/amazon-corretto-17.0.10.8.1-linux-x64 30 | export PATH=$JAVA_HOME/bin:$PATH 31 | fi 32 | fi 33 | 34 | if type -p mvn; then 35 | echo found mvn executable in PATH 36 | _mvn=mvn 37 | elif [[ -n "$MAVEN_HOME" ]] && [[ -x "$MAVEN_HOME/bin/mvn" ]]; then 38 | echo found mvn executable in MAVEN_HOME 39 | _mvn="$MAVEN_HOME/bin/mvn" 40 | else 41 | echo installing maven 3.9.6 42 | curl -LJO https://dlcdn.apache.org/maven/maven-3/3.9.6/binaries/apache-maven-3.9.6-bin.zip 43 | unzip apache-maven-3.9.6-bin.zip 44 | export MAVEN_HOME=$(pwd)/apache-maven-3.9.6 45 | export PATH=$MAVEN_HOME/bin:$PATH 46 | fi 47 | 48 | if [[ "$_mvn" ]]; then 49 | version=$("$_mvn" --version 2>&1 | awk -F'[ "]' '/Maven/ {print $3}') 50 | echo version "$version" 51 | if [[ "$version" > "3.2.5" ]]; then 52 | echo version is greater than 3.2.5, meets requirement 53 | else 54 | echo installing maven 3.9.6 55 | curl -LJO https://dlcdn.apache.org/maven/maven-3/3.9.6/binaries/apache-maven-3.9.6-bin.zip 56 | unzip apache-maven-3.9.6-bin.zip 57 | export MAVEN_HOME=$(pwd)/apache-maven-3.9.6 58 | export PATH=$MAVEN_HOME/bin:$PATH 59 | fi 60 | fi 61 | 62 | export AGENT=https://github.com/[ACCOUNT]/[REPOSITORY]/archive/[BRANCH].zip 63 | echo downloading Gamelift Agent from AGENT 64 | curl -LJO AGENT 65 | unzip [REPOSITORY]-[BRANCH].zip 66 | cd [REPOSITORY]-[BRANCH] 67 | mvn clean compile assembly:single -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/model/websocket/RefreshConnectionMessageTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model.websocket; 5 | 6 | import com.amazon.gamelift.agent.module.ProcessModule; 7 | import com.amazon.gamelift.agent.websocket.handlers.RefreshConnectionHandler; 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import com.fasterxml.jackson.core.JsonGenerator; 10 | import com.fasterxml.jackson.databind.DeserializationFeature; 11 | import com.fasterxml.jackson.databind.MapperFeature; 12 | import com.fasterxml.jackson.databind.ObjectMapper; 13 | import com.fasterxml.jackson.databind.SerializationFeature; 14 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 15 | import java.time.Instant; 16 | import org.junit.jupiter.api.Test; 17 | import org.junit.jupiter.api.extension.ExtendWith; 18 | import org.mockito.junit.jupiter.MockitoExtension; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | public class RefreshConnectionMessageTest { 22 | 23 | private static final String REFRESH_ENDPOINT = "www.refresh.com"; 24 | private static final String AUTH_TOKEN = "auth-token-12345"; 25 | 26 | // Expiration Timestamp will be sent from GameLift as Epoch Seconds 27 | private static final Instant EXPIRATION_INSTANT = Instant.ofEpochSecond(1702427749L); 28 | 29 | // Get ObjectMapper from the Dagger component to utilize the correct configurations 30 | private final ObjectMapper objectMapper = new ProcessModule().provideObjectMapper(); 31 | 32 | @Test 33 | public void GIVEN_validMessageWithEpochMillis_WHEN_objectMapperDeserialize_THEN_deserializesInstant() throws Exception { 34 | // GIVEN 35 | final String serializedMessage = "{" 36 | + "\"Action\": \"" + RefreshConnectionHandler.ACTION + "\"," 37 | + "\"RefreshConnectionEndpoint\": \"" + REFRESH_ENDPOINT + "\"," 38 | + "\"AuthToken\": \"" + AUTH_TOKEN + "\"," 39 | + "\"ExpirationTime\": \"" + EXPIRATION_INSTANT.getEpochSecond() + "\"" 40 | + "}"; 41 | 42 | // WHEN 43 | RefreshConnectionMessage deserializedMessage = 44 | objectMapper.readValue(serializedMessage, RefreshConnectionMessage.class); 45 | 46 | // THEN 47 | assertEquals(RefreshConnectionHandler.ACTION, deserializedMessage.getAction()); 48 | assertEquals(REFRESH_ENDPOINT, deserializedMessage.getRefreshConnectionEndpoint()); 49 | assertEquals(AUTH_TOKEN, deserializedMessage.getAuthToken()); 50 | assertEquals(EXPIRATION_INSTANT, deserializedMessage.getExpirationTime()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/cache/RuntimeConfigCacheLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.cache; 5 | 6 | import com.amazon.gamelift.agent.model.RuntimeConfiguration; 7 | import com.amazon.gamelift.agent.model.websocket.DescribeRuntimeConfigurationRequest; 8 | import com.amazon.gamelift.agent.model.websocket.DescribeRuntimeConfigurationResponse; 9 | import com.amazon.gamelift.agent.model.exception.NotFoundException; 10 | import com.amazon.gamelift.agent.model.exception.AgentException; 11 | import com.amazon.gamelift.agent.websocket.WebSocketConnectionProvider; 12 | import com.google.common.cache.CacheLoader; 13 | 14 | import lombok.NonNull; 15 | import lombok.extern.slf4j.Slf4j; 16 | 17 | import javax.inject.Inject; 18 | import java.time.Duration; 19 | 20 | @Slf4j 21 | public class RuntimeConfigCacheLoader extends CacheLoader { 22 | private final WebSocketConnectionProvider webSocketConnectionProvider; 23 | private static final Duration REFRESH_TIMEOUT = Duration.ofMinutes(1); 24 | 25 | /** 26 | * Constructor for RuntimeConfigCacheLoader 27 | * @param webSocketConnectionProvider 28 | */ 29 | @Inject 30 | public RuntimeConfigCacheLoader(final WebSocketConnectionProvider webSocketConnectionProvider) { 31 | this.webSocketConnectionProvider = webSocketConnectionProvider; 32 | } 33 | 34 | @Override 35 | public @NonNull RuntimeConfiguration load(final @NonNull String key) throws AgentException { 36 | log.info("Sending DescribeRuntimeConfigurationRequest."); 37 | final DescribeRuntimeConfigurationResponse response = webSocketConnectionProvider.getCurrentConnection().sendRequest( 38 | new DescribeRuntimeConfigurationRequest(), DescribeRuntimeConfigurationResponse.class, REFRESH_TIMEOUT); 39 | if (response != null) { 40 | final RuntimeConfiguration loadedRuntimeConfiguration = RuntimeConfiguration.builder() 41 | .gameSessionActivationTimeoutSeconds(response.getGameSessionActivationTimeoutSeconds()) 42 | .maxConcurrentGameSessionActivations(response.getMaxConcurrentGameSessionActivations()) 43 | .serverProcesses(response.getServerProcesses()) 44 | .build(); 45 | log.info("DescribeRuntimeConfigurationResponse received. Updating runtime config to: {}", 46 | loadedRuntimeConfiguration); 47 | return loadedRuntimeConfiguration; 48 | } 49 | throw new NotFoundException("Failed to load non-empty runtime configuration from Amazon GameLift. " 50 | + "Runtime configuration cannot be null"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/logging/UploadGameSessionLogsCallableFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.logging; 5 | 6 | import com.amazon.gamelift.agent.module.ConfigModule; 7 | 8 | import javax.inject.Inject; 9 | import javax.inject.Named; 10 | import java.util.List; 11 | 12 | @Named 13 | public class UploadGameSessionLogsCallableFactory { 14 | 15 | private final String gameSessionLogBucket; 16 | private final String fleetId; 17 | private final String computeName; 18 | private final S3FileUploader s3FileUploader; 19 | private final GameSessionLogFileHelper gameSessionLogFileHelper; 20 | 21 | /** 22 | * Constructor for UploadGameSessionLogsCallableFactory 23 | * @param fleetId 24 | * @param computeName 25 | */ 26 | @Inject 27 | public UploadGameSessionLogsCallableFactory(@Named(ConfigModule.GAME_SESSION_LOG_BUCKET) final String gsLogBucket, 28 | @Named(ConfigModule.FLEET_ID) final String fleetId, 29 | @Named(ConfigModule.COMPUTE_NAME) final String computeName, 30 | final S3FileUploader s3FileUploader, 31 | final GameSessionLogFileHelper gameSessionLogFileHelper) { 32 | this.gameSessionLogBucket = gsLogBucket; 33 | this.fleetId = fleetId; 34 | this.computeName = computeName; 35 | this.s3FileUploader = s3FileUploader; 36 | this.gameSessionLogFileHelper = gameSessionLogFileHelper; 37 | } 38 | 39 | /** 40 | * Creates and returns a UploadGameSessionLogsCallable 41 | * @param processUUID 42 | * @param launchPath 43 | * @return 44 | */ 45 | public UploadGameSessionLogsCallable newUploadGameSessionLogsCallable(final String processUUID, 46 | final String launchPath, 47 | final List logPaths, 48 | final String gameSessionId) { 49 | // Collect the game session logs 50 | final GameSessionLogsCollector gameSessionLogsCollector = 51 | new GameSessionLogsCollector(fleetId, computeName, processUUID, launchPath, gameSessionLogFileHelper); 52 | // Create a Callable to attempt uploading GameSession logs to S3 53 | return new UploadGameSessionLogsCallable(gameSessionLogBucket, fleetId, computeName, processUUID, logPaths, 54 | gameSessionId, s3FileUploader, gameSessionLogsCollector); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/process/builder/WindowsProcessCommons.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.process.builder; 5 | 6 | import com.sun.jna.platform.win32.Kernel32; 7 | import com.sun.jna.platform.win32.Tlhelp32; 8 | import com.sun.jna.platform.win32.WinDef; 9 | import com.sun.jna.platform.win32.WinNT; 10 | import lombok.experimental.UtilityClass; 11 | import lombok.extern.slf4j.Slf4j; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.stream.Stream; 16 | 17 | @Slf4j 18 | @UtilityClass 19 | public class WindowsProcessCommons { 20 | static final int FORCE_TERMINATED_EXIT_CODE = -1; 21 | 22 | /** 23 | * Returns all processes that are a child of a given process on Windows-based machines 24 | * 25 | * @param processId The parent process ID 26 | * @return A stream of child processes handles 27 | */ 28 | public static Stream getChildren(final long processId) { 29 | final List output = new ArrayList<>(); 30 | 31 | /* 32 | Function makes a call to the Win32 kernel to snapshot the current list of processes on the machine. Then iterate over them, and if a given process's 33 | parent is the passed 'processUuid', then store and return it. There is almost no documentation for the JNA library in Java, so instead it's best 34 | to look up C++ documentation, and adapt the code to Java. This code is adapted from: 35 | https://stackoverflow.com/questions/1173342/terminate-a-process-tree-c-for-windows 36 | */ 37 | 38 | final WinNT.HANDLE handleSnapshot = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPPROCESS, new WinDef.DWORD(0)); 39 | final Tlhelp32.PROCESSENTRY32.ByReference processEntry32Ref = new Tlhelp32.PROCESSENTRY32.ByReference(); 40 | 41 | if (!Kernel32.INSTANCE.Process32First(handleSnapshot, processEntry32Ref)) { 42 | return Stream.empty(); 43 | } 44 | do { 45 | if (processEntry32Ref.th32ParentProcessID.longValue() == processId) { 46 | final WinDef.DWORD childProcessId = processEntry32Ref.th32ProcessID; 47 | final WinNT.HANDLE childProcessHandle = Kernel32.INSTANCE.OpenProcess(WinNT.PROCESS_ALL_ACCESS, false, childProcessId.intValue()); 48 | if (childProcessHandle != null) { 49 | output.add(new WindowsProcessHandle(childProcessId, childProcessHandle)); 50 | } 51 | } 52 | } while (Kernel32.INSTANCE.Process32Next(handleSnapshot, processEntry32Ref)); 53 | 54 | Kernel32.INSTANCE.CloseHandle(handleSnapshot); 55 | 56 | return output.stream(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/websocket/handlers/ForceExitProcessHandlerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.websocket.handlers; 5 | 6 | import com.amazon.gamelift.agent.model.ProcessTerminationReason; 7 | import com.amazon.gamelift.agent.model.websocket.ForceExitProcessMessage; 8 | import com.amazon.gamelift.agent.process.GameProcessManager; 9 | import com.amazon.gamelift.agent.model.constants.WebSocketActions; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.extension.ExtendWith; 14 | import org.mockito.Mock; 15 | import org.mockito.junit.jupiter.MockitoExtension; 16 | 17 | import static org.mockito.ArgumentMatchers.any; 18 | import static org.mockito.Mockito.never; 19 | import static org.mockito.Mockito.verify; 20 | 21 | @Deprecated 22 | @ExtendWith(MockitoExtension.class) 23 | public class ForceExitProcessHandlerTest { 24 | 25 | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); 26 | private static final String PROCESS_ID = "ProcessId"; 27 | 28 | @Mock 29 | private GameProcessManager gameProcessManager; 30 | 31 | private ForceExitProcessMessage message; 32 | private String messageAsString; 33 | 34 | private ForceExitProcessHandler forceExitProcessHandler; 35 | 36 | @BeforeEach 37 | public void setup() { 38 | forceExitProcessHandler = new ForceExitProcessHandler(OBJECT_MAPPER, gameProcessManager); 39 | } 40 | 41 | @Test 42 | public void GIVEN_terminateProcessMessage_WHEN_handle_THEN_processTerminated() throws Exception { 43 | //Givens 44 | message = new ForceExitProcessMessage(); 45 | message.setProcessId(PROCESS_ID); 46 | message.setAction(WebSocketActions.ForceExitProcess.name()); 47 | messageAsString = OBJECT_MAPPER.writeValueAsString(message); 48 | 49 | //When 50 | forceExitProcessHandler.handle(messageAsString); 51 | 52 | //Then 53 | verify(gameProcessManager).terminateProcessByUUID(PROCESS_ID, 54 | ProcessTerminationReason.NORMAL_TERMINATION); 55 | } 56 | 57 | @Test 58 | public void GIVEN_invalidAction_WHEN_handle_THEN_processTerminated() throws Exception { 59 | //Givens 60 | message = new ForceExitProcessMessage(); 61 | message.setProcessId(PROCESS_ID); 62 | message.setAction(""); 63 | messageAsString = OBJECT_MAPPER.writeValueAsString(message); 64 | 65 | //When 66 | forceExitProcessHandler.handle(messageAsString); 67 | 68 | //Then 69 | verify(gameProcessManager, never()).terminateProcessByUUID(any(), any()); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent; 5 | 6 | import static com.amazon.gamelift.agent.Application.JAVA_PROP_FORKJOINPOOL_PARALLELISM_KEY; 7 | import static com.amazon.gamelift.agent.Application.JAVA_PROP_FORKJOINPOOL_PARALLELISM_VALUE; 8 | import static com.amazon.gamelift.agent.Application.JAVA_PROP_NEG_TTL_KEY; 9 | import static com.amazon.gamelift.agent.Application.JAVA_PROP_NEG_TTL_VALUE; 10 | import static com.amazon.gamelift.agent.Application.JAVA_PROP_NETWORK_ADDR_CACHE_TTL_KEY; 11 | import static com.amazon.gamelift.agent.Application.JAVA_PROP_NETWORK_ADDR_CACHE_TTL_VALUE; 12 | import static com.amazon.gamelift.agent.Application.JAVA_PROP_PREFER_IPV4_STACK_KEY; 13 | import static com.amazon.gamelift.agent.Application.JAVA_PROP_PREFER_IPV4_STACK_VALUE; 14 | import static com.amazon.gamelift.agent.Application.JAVA_PROP_SECURITY_EGD_KEY; 15 | import static com.amazon.gamelift.agent.Application.JAVA_PROP_SECURITY_EGD_VALUE; 16 | import static com.amazon.gamelift.agent.Application.JAVA_PROP_TTL_KEY; 17 | import static com.amazon.gamelift.agent.Application.JAVA_PROP_TTL_VALUE; 18 | import static org.junit.jupiter.api.Assertions.assertEquals; 19 | 20 | import java.util.HashMap; 21 | import java.util.Map; 22 | 23 | import lombok.SneakyThrows; 24 | import org.junit.jupiter.api.Test; 25 | 26 | public class ApplicationTest { 27 | 28 | @Test 29 | @SneakyThrows 30 | public void GIVEN_systemProperties_WHEN_mainIsCalled_THEN_javaSystemPropertiesAreSet() { 31 | // GIVEN 32 | final String params = "-r us-west-2 -fleet-id fleet-id -c compute_name -loc location"; 33 | final String[] args = params.split(" "); 34 | 35 | // WHEN 36 | try { 37 | Application.main(args); 38 | } catch (final Exception ex) { 39 | // Do nothing, only testing that the SystemProperties are set in this test. 40 | } 41 | 42 | // THEN 43 | final Map paramMap = new HashMap(); 44 | System.getProperties().forEach((k, v) -> { 45 | paramMap.put(k, v); 46 | }); 47 | 48 | assertEquals(paramMap.get(JAVA_PROP_TTL_KEY), JAVA_PROP_TTL_VALUE); 49 | assertEquals(paramMap.get(JAVA_PROP_NEG_TTL_KEY), JAVA_PROP_NEG_TTL_VALUE); 50 | assertEquals(paramMap.get(JAVA_PROP_NETWORK_ADDR_CACHE_TTL_KEY), JAVA_PROP_NETWORK_ADDR_CACHE_TTL_VALUE); 51 | assertEquals(paramMap.get(JAVA_PROP_PREFER_IPV4_STACK_KEY), JAVA_PROP_PREFER_IPV4_STACK_VALUE); 52 | assertEquals(paramMap.get(JAVA_PROP_FORKJOINPOOL_PARALLELISM_KEY), JAVA_PROP_FORKJOINPOOL_PARALLELISM_VALUE); 53 | assertEquals(paramMap.get(JAVA_PROP_SECURITY_EGD_KEY), JAVA_PROP_SECURITY_EGD_VALUE); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/module/ClientModule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.module; 5 | 6 | import com.amazon.gamelift.agent.utils.AmazonGameLiftRetryCondition; 7 | import com.amazonaws.ClientConfiguration; 8 | import com.amazonaws.PredefinedClientConfigurations; 9 | import com.amazonaws.auth.AWSCredentialsProvider; 10 | import com.amazonaws.client.builder.AwsClientBuilder; 11 | import com.amazonaws.retry.PredefinedRetryPolicies; 12 | import com.amazonaws.retry.RetryPolicy; 13 | import com.amazonaws.services.gamelift.AmazonGameLift; 14 | import com.amazonaws.services.gamelift.AmazonGameLiftClientBuilder; 15 | import dagger.Module; 16 | import dagger.Provides; 17 | 18 | import java.net.http.HttpClient; 19 | import java.net.http.WebSocket; 20 | 21 | import javax.annotation.Nullable; 22 | import javax.inject.Named; 23 | 24 | /** 25 | * Module to provide the dependencies for SDK clients. 26 | */ 27 | @Module 28 | public class ClientModule { 29 | 30 | /** 31 | * Provides AmazonGameLift client 32 | * @param region 33 | * @param gameLiftEndpointOverride 34 | * @param credentialsProvider 35 | * @return 36 | */ 37 | @Provides 38 | public AmazonGameLift provideAmazonGameLift( 39 | @Named(ConfigModule.REGION) final String region, 40 | @Named(ConfigModule.GAMELIFT_ENDPOINT_OVERRIDE) @Nullable final String gameLiftEndpointOverride, 41 | @Named(ConfigModule.GAMELIFT_CREDENTIALS) final AWSCredentialsProvider credentialsProvider) { 42 | final ClientConfiguration clientConfiguration = PredefinedClientConfigurations.defaultConfig() 43 | .withRetryPolicy(new RetryPolicy(new AmazonGameLiftRetryCondition(), 44 | PredefinedRetryPolicies.DEFAULT_BACKOFF_STRATEGY, 45 | PredefinedRetryPolicies.DEFAULT_MAX_ERROR_RETRY, 46 | true, true, true)); 47 | 48 | final AmazonGameLiftClientBuilder amazonGameLiftClientBuilder = AmazonGameLiftClientBuilder.standard() 49 | .withCredentials(credentialsProvider) 50 | .withClientConfiguration(clientConfiguration); 51 | 52 | if (gameLiftEndpointOverride == null) { 53 | amazonGameLiftClientBuilder.setRegion(region); 54 | } else { 55 | amazonGameLiftClientBuilder.setEndpointConfiguration( 56 | new AwsClientBuilder.EndpointConfiguration(gameLiftEndpointOverride, region) 57 | ); 58 | } 59 | 60 | return amazonGameLiftClientBuilder.build(); 61 | } 62 | 63 | /** 64 | * Provides Websocket builder 65 | * @return 66 | */ 67 | @Provides 68 | public WebSocket.Builder provideWebSocketBuilder() { 69 | return HttpClient.newBuilder().build().newWebSocketBuilder(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/AgentTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent; 5 | 6 | import com.amazon.gamelift.agent.logging.GameLiftAgentLogUploader; 7 | import com.amazon.gamelift.agent.manager.HeartbeatSender; 8 | import com.amazon.gamelift.agent.manager.InstanceTerminationMonitor; 9 | import com.amazon.gamelift.agent.manager.ShutdownOrchestrator; 10 | import com.amazon.gamelift.agent.process.GameProcessMonitor; 11 | import com.amazon.gamelift.agent.websocket.WebSocketConnectionManager; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.extension.ExtendWith; 14 | import org.mockito.InjectMocks; 15 | import org.mockito.Mock; 16 | import org.mockito.junit.jupiter.MockitoExtension; 17 | 18 | import static org.junit.jupiter.api.Assertions.assertThrows; 19 | import static org.mockito.Mockito.doThrow; 20 | import static org.mockito.Mockito.verify; 21 | 22 | @ExtendWith(MockitoExtension.class) 23 | public class AgentTest { 24 | @Mock 25 | private WebSocketConnectionManager webSocketConnectionManager; 26 | @Mock 27 | private GameProcessMonitor gameProcessMonitor; 28 | @Mock 29 | private HeartbeatSender heartbeatSender; 30 | @Mock 31 | private InstanceTerminationMonitor instanceTerminationMonitor; 32 | @Mock 33 | private ShutdownOrchestrator shutdownOrchestrator; 34 | @Mock 35 | private GameLiftAgentLogUploader gameLiftAgentLogUploader; 36 | 37 | @InjectMocks 38 | private Agent agent; 39 | 40 | @Test 41 | public void GIVEN_validParams_WHEN_start_THEN_startsAndFinishes() throws Exception { 42 | // WHEN 43 | agent.start(); 44 | 45 | // THEN 46 | verify(webSocketConnectionManager).connect(); 47 | verify(heartbeatSender).start(); 48 | verify(instanceTerminationMonitor).start(); 49 | verify(gameProcessMonitor).start(); 50 | verify(gameLiftAgentLogUploader).start(); 51 | } 52 | 53 | @Test 54 | public void GIVEN_exceptionThrown_WHEN_start_THEN_reraisesException() { 55 | // GIVEN 56 | doThrow(RuntimeException.class).when(webSocketConnectionManager).connect(); 57 | 58 | // WHEN / THEN 59 | assertThrows(RuntimeException.class, () -> agent.start()); 60 | } 61 | 62 | @Test 63 | public void GIVEN_nothing_WHEN_shutdown_THEN_executesShutdownLogic() { 64 | // WHEN 65 | agent.shutdown(); 66 | 67 | // THEN 68 | verify(shutdownOrchestrator).completeTermination(); 69 | } 70 | 71 | @Test 72 | public void GIVEN_exceptionThrown_WHEN_shutdown_THEN_reraisesException() { 73 | // GIVEN 74 | doThrow(RuntimeException.class).when(shutdownOrchestrator).completeTermination(); 75 | 76 | // WHEN / THEN 77 | assertThrows(RuntimeException.class, () -> agent.shutdown()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/manager/ComputeAuthTokenManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.manager; 5 | 6 | import com.amazon.gamelift.agent.model.gamelift.GetComputeAuthTokenResponse; 7 | import com.google.common.cache.LoadingCache; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | import javax.inject.Inject; 11 | import javax.inject.Singleton; 12 | import java.time.Duration; 13 | import java.time.Instant; 14 | import java.util.concurrent.ExecutionException; 15 | 16 | @Slf4j 17 | @Singleton 18 | public class ComputeAuthTokenManager { 19 | private final LoadingCache computeAuthTokenCache; 20 | /** 21 | * When calling Amazon GameLift to get a ComputeAuthToken, if less than 15 minutes remain on expiration a new 22 | * token is generated. If the cached value has less than 15 minutes remaining until expiration it should be 23 | * invalidated to force a cache load. 24 | * 25 | * Cache expiration time is set to 5 minutes so that a call is made to Amazon GameLift GetComputeAuthToken 26 | * at least once every 5 minutes just as a precaution to check for any potential issue with the existing token. 27 | */ 28 | private static final Duration CACHE_REFRESH_THRESHOLD = Duration.ofMinutes(15); 29 | private static final String COMPUTE_AUTH_TOKEN_CACHE_KEY = "computeAuthToken"; 30 | 31 | /** 32 | * Constructor for ComputeAuthTokenManager 33 | * @param computeAuthTokenCache 34 | */ 35 | @Inject 36 | public ComputeAuthTokenManager(final LoadingCache computeAuthTokenCache) { 37 | this.computeAuthTokenCache = computeAuthTokenCache; 38 | } 39 | 40 | /** 41 | * Load ComputeAuthToken 42 | * @return 43 | * @throws RuntimeException 44 | */ 45 | public synchronized String getComputeAuthToken() throws RuntimeException { 46 | try { 47 | // See documentation at top of class for reason for these actions 48 | final GetComputeAuthTokenResponse authTokenResponse = computeAuthTokenCache.get(COMPUTE_AUTH_TOKEN_CACHE_KEY); 49 | final Instant refreshTime = authTokenResponse.getExpirationTimeEpochMillis().minus(CACHE_REFRESH_THRESHOLD); 50 | if (Instant.now().isAfter(refreshTime)) { 51 | computeAuthTokenCache.invalidateAll(); 52 | return computeAuthTokenCache.get(COMPUTE_AUTH_TOKEN_CACHE_KEY).getAuthToken(); 53 | } else { 54 | return authTokenResponse.getAuthToken(); 55 | } 56 | } catch (final ExecutionException e) { 57 | log.error("Caught an exception while loading computeAuthToken from cache", e); 58 | throw new RuntimeException("Caught an exception while loading computeAuthToken from cache", e); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/websocket/handlers/ForceExitServerProcessHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.websocket.handlers; 5 | 6 | import com.amazon.gamelift.agent.model.ProcessTerminationReason; 7 | import com.amazon.gamelift.agent.model.constants.WebSocketActions; 8 | import com.amazon.gamelift.agent.model.websocket.ForceExitServerProcessMessage; 9 | import com.amazon.gamelift.agent.process.GameProcessManager; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.apache.commons.lang3.StringUtils; 13 | 14 | import javax.inject.Inject; 15 | 16 | @Slf4j 17 | public class ForceExitServerProcessHandler extends MessageHandler { 18 | 19 | private final GameProcessManager gameProcessManager; 20 | private static final ProcessTerminationReason DEFAULT_TERMINATION_REASON = 21 | ProcessTerminationReason.NORMAL_TERMINATION; 22 | 23 | /** 24 | * Constructor for ForceExitServerProcessHandler 25 | * See GameLiftAgentWebSocketListener for message routing by Action 26 | * @param objectMapper 27 | * @param gameProcessManager 28 | */ 29 | @Inject 30 | public ForceExitServerProcessHandler(final ObjectMapper objectMapper, 31 | final GameProcessManager gameProcessManager) { 32 | super(ForceExitServerProcessMessage.class, objectMapper); 33 | this.gameProcessManager = gameProcessManager; 34 | } 35 | 36 | @Override 37 | public void handle(final ForceExitServerProcessMessage message) { 38 | log.info("Forcing server process to exit message received: {}", message); 39 | 40 | if (!WebSocketActions.ForceExitServerProcess.name().equals(message.getAction())) { 41 | log.error("Message is not a ForceExitServerProcess action. Is {}. Not processing", 42 | message.getAction()); 43 | return; 44 | } 45 | ProcessTerminationReason reason; 46 | try { 47 | reason = StringUtils.isBlank(message.getTerminationReason()) 48 | ? DEFAULT_TERMINATION_REASON 49 | : ProcessTerminationReason.valueOf(message.getTerminationReason()); 50 | } catch (final IllegalArgumentException e) { 51 | log.error(String.format("Received ForceExitServerProcess message with unknown termination reason %s. " 52 | + "Using default termination reason %s.", 53 | message.getTerminationReason(), DEFAULT_TERMINATION_REASON)); 54 | reason = DEFAULT_TERMINATION_REASON; 55 | } 56 | 57 | log.info("Force exiting server process with UUID: {}", message.getProcessId()); 58 | 59 | gameProcessManager.terminateProcessByUUID(message.getProcessId(), 60 | reason); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/utils/EcsMetadataReaderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.utils; 5 | 6 | import com.google.gson.Gson; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | import java.net.http.HttpClient; 14 | import java.net.http.HttpResponse; 15 | 16 | import static org.junit.jupiter.api.Assertions.assertEquals; 17 | import static org.mockito.ArgumentMatchers.any; 18 | import static org.mockito.Mockito.when; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | public class EcsMetadataReaderTest { 22 | private static final String TEST_CONTAINER_ID = "0206b271-b33f-47ab-86c6-a0ba208a70a9"; 23 | private static final String JSON_BODY_WITH_CONTAINER_ARN = "{\"DockerId\": \"ea32192c8553fbff06c9340478a2ff089b2bb5646fb718b4ee206641c9086d66\"," 24 | + "\"ContainerARN\": \"arn:aws:ecs:us-west-2:111122223333:container/" + TEST_CONTAINER_ID + "\"}"; 25 | private static final String TEST_TASK_ID = "158d1c8083dd49d6b527399fd6414f5c"; 26 | private static final String JSON_BODY_WITH_TASK_ARN = "{\"Cluster\": \"default\",\n" 27 | + "\"TaskARN\": \"arn:aws:ecs:us-west-2:111122223333:task/default/" + TEST_TASK_ID + "\"}"; 28 | 29 | @Mock private HttpClient mockHttpClient; 30 | @Mock private SystemEnvironmentProvider mockSystemEnvironmentProvider; 31 | @Mock private HttpResponse mockHttpResponse; 32 | 33 | private EcsMetadataReader ecsMetadataReader; 34 | 35 | @BeforeEach 36 | public void setup() { 37 | ecsMetadataReader = new EcsMetadataReader( 38 | new Gson(), 39 | mockHttpClient, 40 | mockSystemEnvironmentProvider); 41 | when(mockSystemEnvironmentProvider.getenv(any())).thenReturn("http://169.254.170.2"); 42 | } 43 | 44 | @Test 45 | public void GIVEN_jsonBody_WHEN_getContainerId_THEN_idReturned() throws Exception { 46 | // GIVEN 47 | when(mockHttpClient.send(any(), any())).thenReturn(mockHttpResponse); 48 | when(mockHttpResponse.body()).thenReturn(JSON_BODY_WITH_CONTAINER_ARN); 49 | 50 | // WHEN 51 | final String containerId = ecsMetadataReader.getContainerId(); 52 | 53 | // THEN 54 | assertEquals(TEST_CONTAINER_ID, containerId); 55 | } 56 | 57 | @Test 58 | public void GIVEN_jsonBody_WHEN_getTaskId_THEN_idReturned() throws Exception { 59 | // GIVEN 60 | when(mockHttpClient.send(any(), any())).thenReturn(mockHttpResponse); 61 | when(mockHttpResponse.body()).thenReturn(JSON_BODY_WITH_TASK_ARN); 62 | 63 | // WHEN 64 | final String taskId = ecsMetadataReader.getTaskId(); 65 | 66 | // THEN 67 | assertEquals(TEST_TASK_ID, taskId); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/cache/FleetRoleCredentialsCacheLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.cache; 5 | 6 | import com.amazon.gamelift.agent.model.FleetRoleCredentialsConfiguration; 7 | import com.amazon.gamelift.agent.model.websocket.GetFleetRoleCredentialsRequest; 8 | import com.amazon.gamelift.agent.model.websocket.GetFleetRoleCredentialsResponse; 9 | import com.amazon.gamelift.agent.model.exception.NotFoundException; 10 | import com.amazon.gamelift.agent.model.exception.AgentException; 11 | import com.amazon.gamelift.agent.websocket.WebSocketConnectionProvider; 12 | import com.google.common.cache.CacheLoader; 13 | 14 | import lombok.NonNull; 15 | import lombok.extern.slf4j.Slf4j; 16 | 17 | import javax.inject.Inject; 18 | import java.time.Duration; 19 | 20 | @Slf4j 21 | public class FleetRoleCredentialsCacheLoader extends CacheLoader { 22 | 23 | private final WebSocketConnectionProvider webSocketConnectionProvider; 24 | private static final Duration REFRESH_TIMEOUT = Duration.ofMinutes(1); 25 | 26 | /** 27 | * Constructor for FleetRoleCredentialsCacheLoader 28 | * @param webSocketConnectionProvider 29 | */ 30 | @Inject 31 | public FleetRoleCredentialsCacheLoader(final WebSocketConnectionProvider webSocketConnectionProvider) { 32 | this.webSocketConnectionProvider = webSocketConnectionProvider; 33 | } 34 | 35 | @Override 36 | public @NonNull FleetRoleCredentialsConfiguration load(final @NonNull String key) throws AgentException { 37 | log.info("Sending GetFleetRoleCredentials Request."); 38 | final GetFleetRoleCredentialsResponse response = webSocketConnectionProvider.getCurrentConnection() 39 | .sendRequest(new GetFleetRoleCredentialsRequest(), GetFleetRoleCredentialsResponse.class, REFRESH_TIMEOUT); 40 | if (response != null) { 41 | final FleetRoleCredentialsConfiguration loadedFleetRoleCredentialsConfiguration = FleetRoleCredentialsConfiguration.builder() 42 | .assumedRoleUserArn(response.getAssumedRoleUserArn()) 43 | .assumedRoleId(response.getAssumedRoleId()) 44 | .accessKeyId(response.getAccessKeyId()) 45 | .secretAccessKey(response.getSecretAccessKey()) 46 | .sessionToken(response.getSessionToken()) 47 | .expiration(response.getExpiration()) 48 | .build(); 49 | log.info("GetFleetRoleCredentialsResponse received. Updating FleetRoleCredentials- {}", 50 | loadedFleetRoleCredentialsConfiguration); 51 | return loadedFleetRoleCredentialsConfiguration; 52 | } 53 | throw new NotFoundException("Failed to load non-empty FleetRoleCredentials configuration from Amazon GameLift. " 54 | + "FleetRoleCredentials cannot be null"); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/utils/RetryHelperTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.utils; 5 | 6 | import java.util.concurrent.Callable; 7 | 8 | import com.amazon.gamelift.agent.model.exception.InvalidRequestException; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | import org.junit.jupiter.api.Test; 12 | import org.mockito.Mock; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | import static org.junit.jupiter.api.Assertions.assertThrows; 16 | import static org.mockito.Mockito.times; 17 | import static org.mockito.Mockito.verify; 18 | import static org.mockito.Mockito.when; 19 | 20 | import com.amazon.gamelift.agent.model.exception.InternalServiceException; 21 | 22 | @ExtendWith(MockitoExtension.class) 23 | public class RetryHelperTest { 24 | @Mock private Callable failsOnceFunc; 25 | 26 | @BeforeEach 27 | public void setup() { 28 | RetryHelper.disableBackoff(); 29 | } 30 | 31 | @Test 32 | public void GIVEN_oneException_WHEN_runRetryable_THEN_retriesAndSucceeds() throws Exception { 33 | // GIVEN 34 | final int numRetries = 1; 35 | final String successString = "SUCCESS"; 36 | when(failsOnceFunc.call()).thenThrow(new RuntimeException()).thenReturn(successString); 37 | 38 | // WHEN 39 | String result = RetryHelper.runRetryable(numRetries, false, failsOnceFunc); 40 | 41 | // THEN 42 | verify(failsOnceFunc, times(numRetries + 1)).call(); 43 | assertEquals(result, successString); 44 | } 45 | 46 | @Test 47 | public void GIVEN_multipleException_WHEN_runRetryable_THEN_throws() throws Exception { 48 | // GIVEN 49 | final int numRetries = 5; 50 | when(failsOnceFunc.call()).thenThrow(new RuntimeException()); 51 | 52 | // WHEN/Then 53 | assertThrows(RuntimeException.class, () -> RetryHelper.runRetryable(numRetries, false, failsOnceFunc)); 54 | verify(failsOnceFunc, times(numRetries + 1)).call(); 55 | } 56 | 57 | @Test 58 | public void GIVEN_invalidRequestException_WHEN_runRetryable_THEN_throws() throws Exception { 59 | // GIVEN 60 | when(failsOnceFunc.call()).thenThrow(new InvalidRequestException("test!")); 61 | 62 | // WHEN 63 | assertThrows(InvalidRequestException.class, () -> RetryHelper.runRetryable(failsOnceFunc)); 64 | 65 | // THEN 66 | verify(failsOnceFunc, times(1)).call(); 67 | } 68 | 69 | @Test 70 | public void GIVEN_internalServiceException_WHEN_runRetryable_THEN_retriesAndThrows() throws Exception { 71 | // GIVEN 72 | when(failsOnceFunc.call()).thenThrow(new InternalServiceException("test!")); 73 | 74 | // WHEN 75 | assertThrows(InternalServiceException.class, () -> RetryHelper.runRetryable(failsOnceFunc)); 76 | 77 | // THEN 78 | verify(failsOnceFunc, times(3)).call(); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/model/RuntimeConfigurationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.Arrays; 9 | import java.util.Collections; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertThrows; 13 | import static org.junit.jupiter.api.Assertions.fail; 14 | 15 | public class RuntimeConfigurationTest { 16 | @Test 17 | public void GIVEN_allValues_WHEN_builder_THEN_success() { 18 | // GIVEN 19 | final Integer gameSessionActivationTimeout = 100; 20 | final Integer maxConcurrentSessions = 10; 21 | final GameProcessConfiguration process = GameProcessConfiguration.builder().concurrentExecutions(1).launchPath("test").build(); 22 | final GameProcessConfiguration process2 = GameProcessConfiguration.builder(). 23 | concurrentExecutions(2). 24 | launchPath("test2"). 25 | parameters("test") 26 | .build(); 27 | // WHEN 28 | final RuntimeConfiguration config = RuntimeConfiguration.builder() 29 | .gameSessionActivationTimeoutSeconds(gameSessionActivationTimeout) 30 | .maxConcurrentGameSessionActivations(maxConcurrentSessions) 31 | .serverProcesses(Arrays.asList(process, process2)) 32 | .build(); 33 | // THEN 34 | assertEquals(gameSessionActivationTimeout, config.getGameSessionActivationTimeoutSeconds()); 35 | assertEquals(maxConcurrentSessions, config.getMaxConcurrentGameSessionActivations()); 36 | assertEquals(2, config.getServerProcesses().size()); 37 | assertEquals(process, config.getServerProcesses().get(0)); 38 | assertEquals(process2, config.getServerProcesses().get(1)); 39 | } 40 | 41 | @Test 42 | public void GIVEN_onlyRequiredFields_WHEN_builder_THEN_success() { 43 | // GIVEN 44 | final GameProcessConfiguration process = GameProcessConfiguration.builder().concurrentExecutions(1).launchPath("test").build(); 45 | // WHEN 46 | final RuntimeConfiguration config = RuntimeConfiguration.builder().serverProcesses(Collections.singletonList(process)).build(); 47 | // THEN 48 | assertEquals(process, config.getServerProcesses().get(0)); 49 | } 50 | 51 | @Test 52 | public void GIVEN_missingServerProcess_WHHEN_builder_THEN_failure() { 53 | // GIVEN 54 | // WHEN 55 | try { 56 | RuntimeConfiguration.builder().build(); 57 | fail("RuntimeConfiguration must have non null server processes."); 58 | } catch (final NullPointerException e) { 59 | // THEN 60 | } 61 | } 62 | 63 | @Test 64 | public void GIVEN_noConcurrentExecutions_WHEN_builder_THEN_throws() { 65 | // THEN 66 | assertThrows(NullPointerException.class, ()-> GameProcessConfiguration.builder().launchPath("test").build()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/utils/AmazonGameLiftRetryConditionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.utils; 5 | 6 | import com.amazonaws.AmazonClientException; 7 | import com.amazonaws.AmazonServiceException; 8 | import com.amazonaws.services.gamelift.model.ConflictException; 9 | import com.amazonaws.services.gamelift.model.InternalServiceException; 10 | import com.amazonaws.services.gamelift.model.InvalidRequestException; 11 | import com.amazonaws.services.gamelift.model.NotFoundException; 12 | import com.amazonaws.services.gamelift.model.NotReadyException; 13 | import com.amazonaws.services.gamelift.model.UnauthorizedException; 14 | import org.apache.http.HttpStatus; 15 | import org.junit.jupiter.params.ParameterizedTest; 16 | import org.junit.jupiter.params.provider.MethodSource; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | import static org.junit.jupiter.api.Assertions.assertEquals; 22 | 23 | public class AmazonGameLiftRetryConditionTest { 24 | 25 | final AmazonGameLiftRetryCondition retryCondition = new AmazonGameLiftRetryCondition(); 26 | 27 | @ParameterizedTest 28 | @MethodSource("exceptionUseCases") 29 | public void GIVEN_exception_WHEN_shouldRetry_THEN_expect(final AmazonClientException exception, 30 | final boolean shouldRetry) { 31 | final boolean output = retryCondition.shouldRetry(null, exception, 0); 32 | assertEquals(shouldRetry, output); 33 | } 34 | 35 | public static Object[][] exceptionUseCases() { 36 | final List testCases = new ArrayList<>(); 37 | // Expected retries 38 | testCases.add(new Object[] {new ConflictException("ConflictException"), true}); 39 | testCases.add(new Object[] {new InternalServiceException("InternalServiceException"), true}); 40 | testCases.add(new Object[] {new NotFoundException("NotFoundException"), true}); 41 | testCases.add(new Object[] {new NotReadyException("NotReadyException"), true}); 42 | AmazonServiceException throttlingException = new AmazonServiceException("ThrottlingException"); 43 | throttlingException.setStatusCode(HttpStatus.SC_TOO_MANY_REQUESTS); 44 | testCases.add(new Object[] {throttlingException, true}); 45 | AmazonServiceException badGatewayException = new AmazonServiceException("BadGatewayException"); 46 | badGatewayException.setStatusCode(HttpStatus.SC_BAD_GATEWAY); 47 | testCases.add(new Object[] {badGatewayException, true}); 48 | // Do not retry 49 | testCases.add(new Object[] {new InvalidRequestException("InvalidRequestException"), false}); 50 | testCases.add(new Object[] {new UnauthorizedException("UnauthorizedException"), false}); 51 | testCases.add(new Object[] {new AmazonClientException("GenericException"), false}); 52 | testCases.add(new Object[] {new AmazonServiceException("GenericException"), false}); 53 | return testCases.toArray(new Object[testCases.size()][]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/logging/LogFileHelperTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.logging; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertFalse; 8 | import static org.junit.jupiter.api.Assertions.assertTrue; 9 | import static org.mockito.Mockito.when; 10 | 11 | import java.io.File; 12 | import java.util.List; 13 | import java.util.Optional; 14 | import java.util.Set; 15 | 16 | import org.junit.jupiter.api.Test; 17 | import org.junit.jupiter.api.extension.ExtendWith; 18 | import org.mockito.InjectMocks; 19 | import org.mockito.Mock; 20 | import org.mockito.junit.jupiter.MockitoExtension; 21 | 22 | @ExtendWith(MockitoExtension.class) 23 | public class LogFileHelperTest { 24 | 25 | private final String ACTIVE_LOG_PATH = "/test/file/path/gameliftagent.log"; 26 | private final String ZIPPED_LOG_PATH = "/test/file/path/oldlog1.log.zip"; 27 | private final String GZIPPED_LOG_PATH = "/test/file/path/oldlog1.log.gz"; 28 | private final File[] FILES_IN_GAMELIFT_AGENT_LOG_DIRECTORY = new File[] { 29 | new File(ACTIVE_LOG_PATH), 30 | new File(ZIPPED_LOG_PATH), 31 | new File(GZIPPED_LOG_PATH) 32 | }; 33 | 34 | @Mock private File mockGameLiftAgentLogDirectory; 35 | @Mock private AgentLogFileFilter mockFileFilter; 36 | 37 | @InjectMocks private LogFileHelper logFileHelper; 38 | 39 | @Test 40 | public void GIVEN_directoryContainingZipAndNonZipFiles_WHEN_getArchivedGameLiftAgentLogs_THEN_onlyReturnsZipFiles() { 41 | when(mockGameLiftAgentLogDirectory.listFiles(mockFileFilter)).thenReturn(FILES_IN_GAMELIFT_AGENT_LOG_DIRECTORY); 42 | 43 | List returnedFiles = logFileHelper.getArchivedGameLiftAgentLogs(); 44 | 45 | assertEquals(2, returnedFiles.size()); 46 | String filePath1 = returnedFiles.get(0).getAbsolutePath(); 47 | String filePath2 = returnedFiles.get(1).getAbsolutePath(); 48 | assertEquals(Set.of(ZIPPED_LOG_PATH, GZIPPED_LOG_PATH), Set.of(filePath1, filePath2)); 49 | } 50 | 51 | @Test 52 | public void GIVEN_directoryContainingZipAndNonZipFiles_WHEN_getActiveGameLiftAgentLog_THEN_onlyReturnsZipFiles() { 53 | when(mockGameLiftAgentLogDirectory.listFiles(mockFileFilter)).thenReturn(FILES_IN_GAMELIFT_AGENT_LOG_DIRECTORY); 54 | 55 | Optional returnedFile = logFileHelper.getActiveGameLiftAgentLog(); 56 | 57 | assertTrue(returnedFile.isPresent()); 58 | assertEquals(ACTIVE_LOG_PATH, returnedFile.get().getAbsolutePath()); 59 | } 60 | 61 | @Test 62 | public void GIVEN_archivedLogFile_WHEN_isArchivedFile_THEN_returnsTrue() { 63 | assertTrue(logFileHelper.isArchivedFile(new File(ZIPPED_LOG_PATH))); 64 | assertTrue(logFileHelper.isArchivedFile(new File(GZIPPED_LOG_PATH))); 65 | } 66 | 67 | @Test 68 | public void GIVEN_nonArchivedLogFile_WHEN_isArchivedFile_THEN_returnsFalse() { 69 | assertFalse(logFileHelper.isArchivedFile(new File(ACTIVE_LOG_PATH))); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/process/builder/ProcessBuilderFactoryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.process.builder; 5 | 6 | import com.amazon.gamelift.agent.model.GameProcessConfiguration; 7 | import com.amazon.gamelift.agent.model.OperatingSystem; 8 | import com.amazon.gamelift.agent.model.OperatingSystemFamily; 9 | 10 | import java.util.Arrays; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.jupiter.MockitoExtension; 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | import static org.junit.jupiter.api.Assertions.assertInstanceOf; 17 | import static org.junit.jupiter.api.Assertions.assertTrue; 18 | import static org.mockito.BDDMockito.given; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | public class ProcessBuilderFactoryTest { 22 | 23 | @Mock 24 | private GameProcessConfiguration mockGameProcessConfiguration; 25 | 26 | @Test 27 | public void GIVEN_os_WHEN_getProcessBuilder_THEN_returnsCorrectProcessBuilder() { 28 | // GIVEN 29 | given(mockGameProcessConfiguration.getLaunchPath()).willReturn("./unitTest"); 30 | given(mockGameProcessConfiguration.getParameters()).willReturn(Arrays.asList("unit", "test")); 31 | 32 | for (final OperatingSystem operatingSystem : OperatingSystem.values()) { 33 | try { 34 | ProcessBuilderWrapper processBuilderWrapper = 35 | ProcessBuilderFactory.getProcessBuilder(mockGameProcessConfiguration, operatingSystem); 36 | if (OperatingSystemFamily.WINDOWS.equals(operatingSystem.getOperatingSystemFamily())) { 37 | // Current implementation of WindowsProcessBuilderWrapper & WindowsProcess requires dlls to exist 38 | // on Compute. This condition will currently not be hit. 39 | assertInstanceOf(WindowsProcessBuilderWrapper.class, processBuilderWrapper); 40 | } else if (OperatingSystemFamily.LINUX.equals(operatingSystem.getOperatingSystemFamily())) { 41 | assertInstanceOf(LinuxProcessBuilderWrapper.class, processBuilderWrapper); 42 | } else { 43 | throw new RuntimeException(String.format("Found operating system family " 44 | + "without ProcessBuilderWrapper configured: %s", 45 | operatingSystem.getOperatingSystemFamily())); 46 | } 47 | } catch (IllegalArgumentException e) { 48 | assertEquals(OperatingSystem.INVALID, operatingSystem); 49 | } catch (UnsatisfiedLinkError | NoClassDefFoundError e) { 50 | // Current implementation of windows code requires the dlls to be present for JNA to even 51 | // create the class. For most dev-cases, developers are running unit tests on linux boxes 52 | // (including pipeline builds). 53 | assertEquals(OperatingSystemFamily.WINDOWS, operatingSystem.getOperatingSystemFamily()); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/manager/DynamicRuntimeConfigurationManagerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.manager; 5 | 6 | import com.amazon.gamelift.agent.model.RuntimeConfiguration; 7 | import com.amazon.gamelift.agent.model.exception.AgentException; 8 | import com.amazon.gamelift.agent.model.websocket.DescribeRuntimeConfigurationResponse; 9 | import com.amazon.gamelift.agent.websocket.AgentWebSocket; 10 | import com.amazon.gamelift.agent.websocket.WebSocketConnectionProvider; 11 | import com.google.common.collect.Lists; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.mockito.Mock; 16 | import org.mockito.junit.jupiter.MockitoExtension; 17 | 18 | import static org.junit.jupiter.api.Assertions.assertEquals; 19 | import static org.mockito.ArgumentMatchers.any; 20 | import static org.mockito.Mockito.times; 21 | import static org.mockito.Mockito.verify; 22 | import static org.mockito.Mockito.when; 23 | 24 | @ExtendWith(MockitoExtension.class) 25 | public class DynamicRuntimeConfigurationManagerTest { 26 | 27 | private static final int TIMEOUT_SECS = 60; 28 | private static final int CONCURRENT_ACTIVATIONS = 5; 29 | private static final RuntimeConfiguration RUNTIME_CONFIG = RuntimeConfiguration.builder() 30 | .gameSessionActivationTimeoutSeconds(TIMEOUT_SECS) 31 | .maxConcurrentGameSessionActivations(CONCURRENT_ACTIVATIONS) 32 | .serverProcesses(Lists.newArrayList()).build(); 33 | 34 | private static final DescribeRuntimeConfigurationResponse RESPONSE = DescribeRuntimeConfigurationResponse.builder() 35 | .gameSessionActivationTimeoutSeconds(TIMEOUT_SECS) 36 | .maxConcurrentGameSessionActivations(CONCURRENT_ACTIVATIONS) 37 | .serverProcesses(Lists.newArrayList()).build(); 38 | @Mock 39 | private AgentWebSocket client; 40 | @Mock 41 | private WebSocketConnectionProvider webSocketConnectionProvider; 42 | private DynamicRuntimeConfigurationManager dynamicRuntimeConfigManager; 43 | 44 | @BeforeEach 45 | public void setup() throws AgentException { 46 | when(webSocketConnectionProvider.getCurrentConnection()).thenReturn(client); 47 | when(client.sendRequest(any(), any(), any())).thenReturn(RESPONSE); 48 | dynamicRuntimeConfigManager = new DynamicRuntimeConfigurationManager(webSocketConnectionProvider); 49 | } 50 | 51 | @Test 52 | public void GIVEN_runtimeConfig_WHEN_created_THEN_runtimeConfigReturned() throws AgentException { 53 | RuntimeConfiguration config = dynamicRuntimeConfigManager.getRuntimeConfiguration(); 54 | assertEquals(config, RUNTIME_CONFIG); 55 | verify(client, times(1)).sendRequest(any(), any(), any()); 56 | } 57 | 58 | @Test 59 | public void GIVEN_runtimeConfig_WHEN_beforeExpiration_THEN_cacheReturned() throws Exception { 60 | RuntimeConfiguration config = dynamicRuntimeConfigManager.getRuntimeConfiguration(); 61 | dynamicRuntimeConfigManager.getRuntimeConfiguration(); 62 | assertEquals(config, RUNTIME_CONFIG); 63 | verify(client, times(1)).sendRequest(any(), any(), any()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/websocket/WebSocketConnectionProviderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.websocket; 5 | 6 | import com.google.common.collect.ImmutableList; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | import java.time.Duration; 14 | import java.util.concurrent.ScheduledExecutorService; 15 | 16 | import static org.junit.jupiter.api.Assertions.assertEquals; 17 | import static org.junit.jupiter.api.Assertions.assertNull; 18 | import static org.mockito.ArgumentMatchers.any; 19 | import static org.mockito.ArgumentMatchers.anyLong; 20 | import static org.mockito.Mockito.mock; 21 | import static org.mockito.Mockito.times; 22 | import static org.mockito.Mockito.verify; 23 | import static org.mockito.Mockito.when; 24 | 25 | @ExtendWith(MockitoExtension.class) 26 | public class WebSocketConnectionProviderTest { 27 | @Mock 28 | private ScheduledExecutorService connectionCloserService; 29 | 30 | private WebSocketConnectionProvider webSocketConnectionProvider; 31 | 32 | @BeforeEach 33 | public void setup() { 34 | webSocketConnectionProvider = new WebSocketConnectionProvider(connectionCloserService); 35 | } 36 | 37 | @Test 38 | public void GIVEN_connection_WHEN_updateConnection_THEN_storesConnection() { 39 | // GIVEN 40 | final AgentWebSocket client = mock(AgentWebSocket.class); 41 | 42 | // WHEN 43 | webSocketConnectionProvider.updateConnection(client); 44 | 45 | // THEN 46 | assertEquals(webSocketConnectionProvider.getCurrentConnection(), client); 47 | } 48 | 49 | @Test 50 | public void GIVEN_existingConnection_WHEN_updateConnection_THEN_storesNewConnectionAndSchedulesClosingOldOne() { 51 | // GIVEN 52 | final AgentWebSocket oldConnection = mock(AgentWebSocket.class); 53 | final AgentWebSocket newConnection = mock(AgentWebSocket.class); 54 | webSocketConnectionProvider.updateConnection(oldConnection); 55 | 56 | // WHEN 57 | webSocketConnectionProvider.updateConnection(newConnection); 58 | 59 | // THEN 60 | assertEquals(webSocketConnectionProvider.getCurrentConnection(), newConnection); 61 | verify(connectionCloserService, times(2)).schedule(any(Runnable.class), anyLong(), any()); 62 | } 63 | 64 | @Test 65 | public void GIVEN_agentShutsDown_WHEN_updateConnection_THEN_closesAllConnections() { 66 | // GIVEN 67 | final AgentWebSocket oldConnection = mock(AgentWebSocket.class); 68 | final AgentWebSocket newConnection = mock(AgentWebSocket.class); 69 | webSocketConnectionProvider.updateConnection(oldConnection); 70 | webSocketConnectionProvider.updateConnection(newConnection); 71 | when(connectionCloserService.shutdownNow()).thenReturn(ImmutableList.of()); 72 | 73 | // WHEN 74 | webSocketConnectionProvider.closeAllConnections(); 75 | 76 | // THEN 77 | assertNull(webSocketConnectionProvider.getCurrentConnection()); 78 | verify(connectionCloserService).shutdownNow(); 79 | verify(newConnection).closeConnection(Duration.ofMinutes(1)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/websocket/handlers/NotifyGameSessionActivatedHandlerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.websocket.handlers; 5 | 6 | import com.amazon.gamelift.agent.model.exception.NotFoundException; 7 | import com.amazon.gamelift.agent.model.websocket.NotifyGameSessionActivatedMessage; 8 | import com.amazon.gamelift.agent.process.GameProcessManager; 9 | import com.amazon.gamelift.agent.model.constants.WebSocketActions; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.extension.ExtendWith; 14 | import org.mockito.Mock; 15 | import org.mockito.Mockito; 16 | import org.mockito.junit.jupiter.MockitoExtension; 17 | 18 | 19 | import static org.mockito.ArgumentMatchers.any; 20 | import static org.mockito.ArgumentMatchers.eq; 21 | import static org.mockito.Mockito.verify; 22 | 23 | @ExtendWith(MockitoExtension.class) 24 | public class NotifyGameSessionActivatedHandlerTest { 25 | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); 26 | private static final String TEST_PROCESS_ID = "TEST_PROCESS_ID"; 27 | private static final String TEST_GAME_SESSION_ID = "TEST_GAME_SESSION_ID"; 28 | 29 | private NotifyGameSessionActivatedMessage message; 30 | private String messageAsString; 31 | 32 | @Mock private GameProcessManager mockGameProcessManager; 33 | 34 | private NotifyGameSessionActivatedHandler notifyGameSessionActivatedHandler; 35 | 36 | @BeforeEach 37 | public void setup() { 38 | notifyGameSessionActivatedHandler = new NotifyGameSessionActivatedHandler(OBJECT_MAPPER, mockGameProcessManager); 39 | } 40 | 41 | @Test 42 | public void GIVEN_processRegisteredMessage_WHEN_handle_THEN_logPathsSaved() throws Exception { 43 | // GIVEN 44 | message = new NotifyGameSessionActivatedMessage(); 45 | message.setProcessId(TEST_PROCESS_ID); 46 | message.setGameSessionId(TEST_GAME_SESSION_ID); 47 | message.setAction(WebSocketActions.NotifyGameSessionActivated.name()); 48 | messageAsString = OBJECT_MAPPER.writeValueAsString(message); 49 | 50 | // WHEN 51 | notifyGameSessionActivatedHandler.handle(messageAsString); 52 | 53 | // THEN 54 | verify(mockGameProcessManager) 55 | .updateProcessOnGameSessionActivation(eq(TEST_PROCESS_ID), eq(TEST_GAME_SESSION_ID)); 56 | } 57 | 58 | @Test 59 | public void GIVEN_notFoundException_WHEN_handle_THEN_exceptionSwallowed() throws Exception { 60 | // GIVEN 61 | message = new NotifyGameSessionActivatedMessage(); 62 | message.setProcessId(TEST_PROCESS_ID); 63 | message.setGameSessionId(TEST_GAME_SESSION_ID); 64 | message.setAction(WebSocketActions.NotifyProcessRegistered.name()); 65 | messageAsString = OBJECT_MAPPER.writeValueAsString(message); 66 | 67 | Mockito.doThrow(NotFoundException.class) 68 | .when(mockGameProcessManager).updateProcessOnGameSessionActivation(any(), any()); 69 | 70 | // WHEN 71 | notifyGameSessionActivatedHandler.handle(messageAsString); 72 | 73 | // THEN 74 | verify(mockGameProcessManager) 75 | .updateProcessOnGameSessionActivation(eq(TEST_PROCESS_ID), eq(TEST_GAME_SESSION_ID)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/manager/InstanceTerminationMonitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.manager; 5 | 6 | import com.amazon.gamelift.agent.utils.ExecutorServiceSafeRunnable; 7 | import com.amazon.gamelift.agent.module.ThreadingModule; 8 | import com.google.common.annotations.VisibleForTesting; 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | import javax.inject.Inject; 12 | import javax.inject.Named; 13 | import java.time.Instant; 14 | import java.time.temporal.ChronoUnit; 15 | import java.util.concurrent.ScheduledExecutorService; 16 | import java.util.concurrent.TimeUnit; 17 | import java.util.concurrent.atomic.AtomicBoolean; 18 | 19 | @Slf4j 20 | public class InstanceTerminationMonitor { 21 | private static final int PROCESS_MANAGER_TERMINATION_LEEWAY_SECONDS = 30; 22 | private static final long INITIAL_DELAY_SECONDS = 5; 23 | private static final long INTERVAL_SECONDS = 5; 24 | 25 | private final AtomicBoolean signalDetected = new AtomicBoolean(false); 26 | 27 | private final TerminationNoticeReader terminationNoticeReader; 28 | private final ScheduledExecutorService executorService; 29 | private final ShutdownOrchestrator shutdownOrchestrator; 30 | 31 | /** 32 | * Constructor for InstanceTerminationMonitor 33 | * @param terminationNoticeReader 34 | * @param shutdownOrchestrator 35 | * @param executorService 36 | */ 37 | @Inject 38 | public InstanceTerminationMonitor( 39 | final TerminationNoticeReader terminationNoticeReader, 40 | final ShutdownOrchestrator shutdownOrchestrator, 41 | @Named(ThreadingModule.INSTANCE_TERMINATION_EXECUTOR) final ScheduledExecutorService executorService) { 42 | this.terminationNoticeReader = terminationNoticeReader; 43 | this.executorService = executorService; 44 | this.shutdownOrchestrator = shutdownOrchestrator; 45 | } 46 | 47 | /** 48 | * Start InstanceTermination monitoring 49 | */ 50 | public void start() { 51 | executorService.scheduleWithFixedDelay(new ExecutorServiceSafeRunnable(this::run), 52 | INITIAL_DELAY_SECONDS, INTERVAL_SECONDS, TimeUnit.SECONDS); 53 | 54 | log.info("Started a daemon to watch for spot instance termination notice every {} seconds.", INTERVAL_SECONDS); 55 | } 56 | 57 | @VisibleForTesting 58 | protected void run() { 59 | try { 60 | final Instant terminationTime = terminationNoticeReader.getTerminationNoticeFromLocalFile() 61 | .orElse(terminationNoticeReader.getTerminationNoticeFromEC2Metadata().orElse(null)); 62 | 63 | if (terminationTime == null) { 64 | return; 65 | } 66 | 67 | if (!signalDetected.compareAndSet(false, true)) { 68 | log.info("Termination notice already detected. Ignoring."); 69 | return; 70 | } 71 | 72 | log.info("Termination notice detected."); 73 | 74 | final Instant terminationDeadline = 75 | terminationTime.minus(PROCESS_MANAGER_TERMINATION_LEEWAY_SECONDS, ChronoUnit.SECONDS); 76 | this.shutdownOrchestrator.startTermination(terminationDeadline, true); 77 | } catch (final Exception e) { 78 | log.error("Failed to check instance for spot termination", e); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/process/destroyer/LinuxProcessDestroyer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.process.destroyer; 5 | 6 | import com.amazon.gamelift.agent.command.CommandTransform; 7 | import com.amazon.gamelift.agent.command.LinuxCommandTransform; 8 | import com.amazon.gamelift.agent.model.OperatingSystem; 9 | import com.amazon.gamelift.agent.model.OperatingSystemFamily; 10 | import com.google.common.annotations.VisibleForTesting; 11 | import com.google.common.collect.ImmutableList; 12 | import lombok.extern.slf4j.Slf4j; 13 | 14 | import javax.inject.Inject; 15 | import javax.inject.Singleton; 16 | import java.io.IOException; 17 | 18 | @Slf4j 19 | @Singleton 20 | public class LinuxProcessDestroyer implements ProcessDestroyer { 21 | private final CommandTransform commandTransform; 22 | private final OperatingSystem operatingSystem; 23 | private final ProcessBuilder processBuilder; 24 | 25 | /** 26 | * Constructor for LinuxProcessDestroyer 27 | * @param operatingSystem 28 | */ 29 | @Inject 30 | public LinuxProcessDestroyer(final OperatingSystem operatingSystem) { 31 | if (!OperatingSystemFamily.LINUX.equals(operatingSystem.getOperatingSystemFamily())) { 32 | // Creation validation. This class should only be used for Linux-based OS 33 | throw new IllegalArgumentException("Attempted to create Linux process for non Linux-based OS. Found " 34 | + operatingSystem); 35 | } 36 | this.operatingSystem = operatingSystem; 37 | this.commandTransform = new LinuxCommandTransform(); 38 | this.processBuilder = new ProcessBuilder(); 39 | } 40 | 41 | /** 42 | * Test constructor for LinuxProcessDestroyer 43 | * @param operatingSystem 44 | */ 45 | @VisibleForTesting 46 | public LinuxProcessDestroyer(final OperatingSystem operatingSystem, 47 | final ProcessBuilder processBuilder) { 48 | if (!OperatingSystemFamily.LINUX.equals(operatingSystem.getOperatingSystemFamily())) { 49 | // Creation validation. This class should only be used for Linux-based OS 50 | throw new IllegalArgumentException("Attempted to create Linux process for non Linux-based OS. Found " 51 | + operatingSystem); 52 | } 53 | this.operatingSystem = operatingSystem; 54 | this.commandTransform = new LinuxCommandTransform(); 55 | this.processBuilder = processBuilder; 56 | } 57 | 58 | @Override 59 | public void destroyProcess(final Process internalProcess) { 60 | final String processGroupId = Long.toString(internalProcess.pid()); 61 | // Use the kill command to forcibly terminate processes with the given Process Group ID (PGID). 62 | // "-9" indicates to forcibly terminate the process. 63 | // "-$PGID" indicates to destroy the entire PGID 64 | // Example command: setsid kill -9 -11111 65 | processBuilder.command( 66 | ImmutableList.of("kill", "-9", String.format("-%s", processGroupId))); 67 | try { 68 | processBuilder.start().waitFor(); 69 | } catch (final IOException | InterruptedException e) { 70 | final String errorMessage = 71 | String.format("Failed to destroy process with PGID %s", processGroupId); 72 | log.error(errorMessage, e); 73 | throw new RuntimeException(errorMessage, e); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/process/builder/LinuxProcessBuilderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.process.builder; 5 | 6 | import com.amazon.gamelift.agent.model.GameProcessConfiguration; 7 | import com.amazon.gamelift.agent.model.OperatingSystem; 8 | import com.amazon.gamelift.agent.model.exception.BadExecutablePathException; 9 | 10 | import java.io.IOException; 11 | import java.util.Map; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.mockito.Mock; 16 | import org.mockito.junit.jupiter.MockitoExtension; 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.junit.jupiter.api.Assertions.assertThrows; 19 | import static org.mockito.Mockito.spy; 20 | import static org.mockito.Mockito.verify; 21 | import static org.mockito.Mockito.when; 22 | 23 | @ExtendWith(MockitoExtension.class) 24 | public class LinuxProcessBuilderTest { 25 | 26 | private static final GameProcessConfiguration PROCESS_CONFIG = GameProcessConfiguration.builder() 27 | .concurrentExecutions(1) 28 | .launchPath("testCommand") 29 | .parameters("--parameter1 --parameter2") 30 | .build(); 31 | private static final OperatingSystem OPERATING_SYSTEM = OperatingSystem.DEFAULT_OS; 32 | 33 | @Mock private ProcessBuilder mockProcessBuilder; 34 | @Mock private Process mockProcess; 35 | @Mock private Map mockEnvironmentVariableMap; 36 | 37 | private LinuxProcessBuilderWrapper linuxProcessBuilderWrapper; 38 | private LinuxProcessBuilderWrapper spyLinuxProcessBuilderWrapper; 39 | 40 | @BeforeEach 41 | public void setup() { 42 | 43 | linuxProcessBuilderWrapper = new LinuxProcessBuilderWrapper(PROCESS_CONFIG, OPERATING_SYSTEM, 44 | mockProcessBuilder); 45 | spyLinuxProcessBuilderWrapper = spy(linuxProcessBuilderWrapper); 46 | } 47 | 48 | @Test 49 | public void GIVEN_validProcessConfigurationWithParameters_WHEN_start_THEN_returnsProcessUuid() throws IOException, BadExecutablePathException { 50 | // GIVEN 51 | when(spyLinuxProcessBuilderWrapper.verifyLaunchFileExists()).thenReturn(true); 52 | when(mockProcessBuilder.start()).thenReturn(mockProcess); 53 | 54 | // WHEN 55 | Process process = spyLinuxProcessBuilderWrapper.buildProcess(mockEnvironmentVariableMap); 56 | 57 | // THEN 58 | assertEquals(process, mockProcess); 59 | verify(mockProcessBuilder).environment(); 60 | verify(mockProcessBuilder).redirectOutput(ProcessBuilder.Redirect.DISCARD); 61 | verify(mockProcessBuilder).redirectError(ProcessBuilder.Redirect.DISCARD); 62 | } 63 | 64 | @Test 65 | public void GIVEN_ioExceptionOnStart_WHEN_start_THEN_returnsProcessUuid() throws IOException { 66 | // GIVEN 67 | when(spyLinuxProcessBuilderWrapper.verifyLaunchFileExists()).thenReturn(true); 68 | when(mockProcessBuilder.start()).thenThrow(new IOException("unit-test")); 69 | 70 | // WHEN 71 | assertThrows(RuntimeException.class, () -> 72 | spyLinuxProcessBuilderWrapper.buildProcess(mockEnvironmentVariableMap)); 73 | } 74 | 75 | @Test 76 | public void GIVEN_processConfigurationWithInvalidPath_WHEN_buildProcess_THEN_throwException() { 77 | // BadExecutablePathException is thrown since "testCommand" file specified for launchPath doesn't exist 78 | assertThrows(BadExecutablePathException.class, () -> 79 | linuxProcessBuilderWrapper.buildProcess(mockEnvironmentVariableMap)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/logging/LogFileHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.logging; 5 | 6 | import java.io.File; 7 | import java.io.FileFilter; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.Objects; 11 | import java.util.Optional; 12 | import java.util.stream.Collectors; 13 | 14 | import javax.inject.Inject; 15 | import javax.inject.Named; 16 | import com.amazon.gamelift.agent.manager.LogConfigurationManager; 17 | import org.apache.logging.log4j.LogManager; 18 | import org.apache.logging.log4j.core.Appender; 19 | import org.apache.logging.log4j.core.Logger; 20 | import org.apache.logging.log4j.core.LoggerContext; 21 | import org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender; 22 | 23 | import static com.amazon.gamelift.agent.module.ConfigModule.GAMELIFT_AGENT_LOGS_DIRECTORY; 24 | 25 | /** 26 | * Helper class for methods related to locating/handling log files on the Compute 27 | */ 28 | public class LogFileHelper { 29 | 30 | private static final String ZIP_EXTENSION = ".zip"; 31 | private static final String GZIP_EXTENSION = ".gz"; 32 | 33 | private final File gameLiftAgentLogDirectory; 34 | private final FileFilter gameLiftAgentLogFileFilter; 35 | 36 | /** 37 | * Constructor for LogFileHelper 38 | * @param gameLiftAgentLogDirectory 39 | * @param agentLogFileFilter 40 | */ 41 | @Inject 42 | public LogFileHelper(@Named(GAMELIFT_AGENT_LOGS_DIRECTORY) final File gameLiftAgentLogDirectory, 43 | final AgentLogFileFilter agentLogFileFilter) { 44 | this.gameLiftAgentLogDirectory = gameLiftAgentLogDirectory; 45 | this.gameLiftAgentLogFileFilter = agentLogFileFilter; 46 | } 47 | 48 | /** 49 | * Returns list of GameLiftAgent logs (archived) 50 | * @return 51 | */ 52 | public List getArchivedGameLiftAgentLogs() { 53 | return Arrays.stream(Objects.requireNonNull(gameLiftAgentLogDirectory.listFiles(gameLiftAgentLogFileFilter))) 54 | .filter(this::isArchivedFile) 55 | .collect(Collectors.toList()); 56 | } 57 | 58 | /** 59 | * Gets the active GameLiftAgent log. 60 | * The active log file's name should match the configured base log name. This function assumes there will only 61 | * ever be one active log file in this directory. 62 | * @return an optional of the active log file 63 | */ 64 | public Optional getActiveGameLiftAgentLog() { 65 | return Arrays.stream(Objects.requireNonNull(gameLiftAgentLogDirectory.listFiles(gameLiftAgentLogFileFilter))) 66 | .filter(file -> LogConfigurationManager.APPLICATION_LOG_FILE_BASENAME.equals(file.getName())) 67 | .findFirst(); 68 | } 69 | 70 | /** 71 | * Returns true if GameLiftAgent log file is archived 72 | * @param fileToVerify 73 | * @return 74 | */ 75 | public boolean isArchivedFile(final File fileToVerify) { 76 | return fileToVerify.getName().endsWith(ZIP_EXTENSION) || fileToVerify.getName().endsWith(GZIP_EXTENSION); 77 | } 78 | 79 | /** 80 | * Will push all logs currently in memory buffers out to their respective files 81 | */ 82 | public static void flushLogBuffers() { 83 | final LoggerContext logCtx = ((LoggerContext) LogManager.getContext()); 84 | for (final Logger logger : logCtx.getLoggers()) { 85 | for (final Appender appender : logger.getAppenders().values()) { 86 | if (appender instanceof AbstractOutputStreamAppender) { 87 | ((AbstractOutputStreamAppender) appender).getManager().flush(); 88 | } 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/model/ConfiguredLogPaths.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.model; 5 | 6 | import com.amazon.gamelift.agent.logging.GameSessionLogPath; 7 | 8 | import java.nio.file.InvalidPathException; 9 | import java.nio.file.Path; 10 | import java.nio.file.Paths; 11 | import java.util.ArrayList; 12 | import java.util.HashSet; 13 | import java.util.List; 14 | import java.util.Set; 15 | import org.apache.commons.io.FilenameUtils; 16 | 17 | import lombok.Getter; 18 | import lombok.RequiredArgsConstructor; 19 | import lombok.extern.slf4j.Slf4j; 20 | 21 | @Slf4j 22 | @Getter 23 | @RequiredArgsConstructor 24 | public class ConfiguredLogPaths { 25 | private final Set validLogPaths = new HashSet<>(); 26 | private final Set invalidLogPaths = new HashSet<>(); 27 | 28 | /** 29 | * Add a valid log path to the collection 30 | * @param logPath 31 | */ 32 | public void addValidLogPath(final String logPath) { 33 | validLogPaths.add(logPath); 34 | } 35 | 36 | /** 37 | * Add an invalid log path to the collection 38 | * @param logPath 39 | */ 40 | public void addInvalidLogPath(final String logPath) { 41 | invalidLogPaths.add(logPath); 42 | } 43 | 44 | /** 45 | * Returns the valid log paths as a list (instead of the set used in this class for duplicate reduction) 46 | * @return List 47 | */ 48 | public List getValidLogPaths() { 49 | return new ArrayList<>(validLogPaths); 50 | } 51 | 52 | /** 53 | * Returns the invalid log paths as a list (instead of the set used in this class for duplicate reduction) 54 | * @return List 55 | */ 56 | public List getInvalidLogPaths() { 57 | return new ArrayList<>(invalidLogPaths); 58 | } 59 | 60 | /** 61 | * Converts a list of Sting log paths into GameSessionLogPath objects 62 | * This object contains the full path for a log path, a relative path to use in the zip folder a filename if and 63 | * only if a wildcard is used (this will later be expanded into individual files for matching files) 64 | * @param logPaths 65 | * @return 66 | */ 67 | public static List convertToGameSessionLogPaths(final List logPaths) { 68 | final List gameSessionLogPaths = new ArrayList<>(); 69 | for (final String path: logPaths) { 70 | try { 71 | /* 72 | * /Removes C:\ from the log path but maintains all the parent folders for destination path So 73 | * C:\app\logs\sample.txt would be copied to $zip\logs\sample.txt 74 | */ 75 | final Path drivePath = Paths.get(FilenameUtils.getPrefix(path)); 76 | 77 | final String fileName = FilenameUtils.getName(path); 78 | if (fileName.contains("*") || fileName.contains("?")) { 79 | final String fullPath = FilenameUtils.getFullPath(path); 80 | Path relativeLogPath = Paths.get(fullPath); 81 | relativeLogPath = drivePath.relativize(relativeLogPath); 82 | gameSessionLogPaths.add(new GameSessionLogPath(fullPath, relativeLogPath.toString(), fileName)); 83 | } else { 84 | Path relativeLogPath = Paths.get(path); 85 | relativeLogPath = drivePath.relativize(relativeLogPath); 86 | gameSessionLogPaths.add(new GameSessionLogPath(path, relativeLogPath.toString())); 87 | } 88 | } catch (final InvalidPathException ipe) { 89 | log.error("Error getting path for " + path, ipe); 90 | } 91 | } 92 | return gameSessionLogPaths; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/cache/ComputeAuthTokenCacheLoaderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.cache; 5 | 6 | import com.amazon.gamelift.agent.model.exception.InternalServiceException; 7 | import com.amazon.gamelift.agent.model.exception.NotFoundException; 8 | import com.amazon.gamelift.agent.model.gamelift.GetComputeAuthTokenResponse; 9 | import com.amazon.gamelift.agent.client.AmazonGameLiftClientWrapper; 10 | import com.amazon.gamelift.agent.model.exception.UnauthorizedException; 11 | import com.amazonaws.services.gamelift.model.GetComputeAuthTokenRequest; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.mockito.Mock; 16 | import org.mockito.junit.jupiter.MockitoExtension; 17 | 18 | import java.time.Duration; 19 | import java.time.Instant; 20 | 21 | import static org.junit.jupiter.api.Assertions.assertEquals; 22 | import static org.junit.jupiter.api.Assertions.assertThrows; 23 | import static org.mockito.ArgumentMatchers.any; 24 | import static org.mockito.Mockito.doThrow; 25 | import static org.mockito.Mockito.lenient; 26 | import static org.mockito.Mockito.when; 27 | 28 | @ExtendWith(MockitoExtension.class) 29 | public class ComputeAuthTokenCacheLoaderTest { 30 | private static final String COMPUTE_AUTH_TOKEN_CACHE_KEY = "computeAuthToken"; 31 | private static final String COMPUTE_AUTH_TOKEN = "AuthToken"; 32 | private static final String FLEET_ID = "fleet-definitelyRealFleet"; 33 | private static final String COMPUTE_NAME = "computeName"; 34 | 35 | @Mock private AmazonGameLiftClientWrapper mockGameLift; 36 | 37 | private ComputeAuthTokenCacheLoader cacheLoader; 38 | 39 | @BeforeEach 40 | public void setup() throws Exception { 41 | final GetComputeAuthTokenResponse response = GetComputeAuthTokenResponse.builder() 42 | .authToken(COMPUTE_AUTH_TOKEN) 43 | .fleetId(FLEET_ID) 44 | .computeName(COMPUTE_NAME) 45 | .expirationTimeEpochMillis(Instant.now().plus(Duration.ofMinutes(15))) 46 | .build(); 47 | cacheLoader = new ComputeAuthTokenCacheLoader(mockGameLift, FLEET_ID, COMPUTE_NAME); 48 | lenient().when(mockGameLift.getComputeAuthToken(any())).thenReturn(response); 49 | } 50 | 51 | @Test 52 | public void GIVEN_exception_WHEN_load_THEN_throwException() throws Exception { 53 | doThrow(UnauthorizedException.class).when(mockGameLift).getComputeAuthToken(any()); 54 | 55 | assertThrows(RuntimeException.class, () -> cacheLoader.load(COMPUTE_AUTH_TOKEN_CACHE_KEY)); 56 | } 57 | 58 | @Test 59 | public void GIVEN_validInput_WHEN_load_THEN_validOutput() { 60 | final String authToken = cacheLoader.load(COMPUTE_AUTH_TOKEN_CACHE_KEY).getAuthToken(); 61 | 62 | assertEquals(COMPUTE_AUTH_TOKEN, authToken); 63 | } 64 | 65 | @Test 66 | public void GIVEN_retryEventuallySucceeds_WHEN_load_THEN_validOutput() throws Exception { 67 | // GIVEN 68 | final GetComputeAuthTokenResponse response = GetComputeAuthTokenResponse.builder() 69 | .authToken(COMPUTE_AUTH_TOKEN) 70 | .fleetId(FLEET_ID) 71 | .computeName(COMPUTE_NAME) 72 | .expirationTimeEpochMillis(Instant.now().plus(Duration.ofMinutes(15))) 73 | .build(); 74 | when(mockGameLift.getComputeAuthToken(any(GetComputeAuthTokenRequest.class))) 75 | .thenThrow(new NotFoundException("Compute not found")) 76 | .thenThrow(new InternalServiceException()) 77 | .thenReturn(response); 78 | 79 | // WHEN 80 | final String authToken = cacheLoader.load(COMPUTE_AUTH_TOKEN_CACHE_KEY).getAuthToken(); 81 | 82 | // THEN 83 | assertEquals(COMPUTE_AUTH_TOKEN, authToken); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tst/com/amazon/gamelift/agent/manager/ComputeAuthTokenManagerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.manager; 5 | 6 | import com.amazon.gamelift.agent.model.gamelift.GetComputeAuthTokenResponse; 7 | import com.google.common.cache.LoadingCache; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | 14 | import java.time.Duration; 15 | import java.time.Instant; 16 | import java.util.concurrent.ExecutionException; 17 | 18 | import static org.junit.jupiter.api.Assertions.assertEquals; 19 | import static org.junit.jupiter.api.Assertions.assertThrows; 20 | import static org.mockito.ArgumentMatchers.eq; 21 | import static org.mockito.Mockito.doThrow; 22 | import static org.mockito.Mockito.lenient; 23 | import static org.mockito.Mockito.times; 24 | import static org.mockito.Mockito.verify; 25 | import static org.mockito.Mockito.when; 26 | 27 | @ExtendWith(MockitoExtension.class) 28 | public class ComputeAuthTokenManagerTest { 29 | @Mock LoadingCache mockComputeAuthTokenCache; 30 | 31 | private static final String FLEET_ID = "fleet-multipleBoats"; 32 | private static final String COMPUTE_NAME = "computeName"; 33 | private static final String COMPUTE_AUTH_TOKEN = "authToken"; 34 | private static final String COMPUTE_AUTH_TOKEN_CACHE_KEY = "computeAuthToken"; 35 | 36 | private ComputeAuthTokenManager computeAuthTokenManager; 37 | 38 | @BeforeEach 39 | public void setup() throws Exception { 40 | final GetComputeAuthTokenResponse authTokenResponse = GetComputeAuthTokenResponse.builder() 41 | .authToken(COMPUTE_AUTH_TOKEN) 42 | .computeName(COMPUTE_NAME) 43 | .fleetId(FLEET_ID) 44 | .expirationTimeEpochMillis(Instant.now().plus(Duration.ofMinutes(20))) 45 | .build(); 46 | lenient().when(mockComputeAuthTokenCache.get(eq(COMPUTE_AUTH_TOKEN_CACHE_KEY))).thenReturn(authTokenResponse); 47 | computeAuthTokenManager = new ComputeAuthTokenManager(mockComputeAuthTokenCache); 48 | } 49 | 50 | @Test 51 | public void GIVEN_computeAuthToken_WHEN_created_THEN_computeAuthToken() throws Exception { 52 | // WHEN 53 | final String authToken = computeAuthTokenManager.getComputeAuthToken(); 54 | 55 | // THEN 56 | assertEquals(authToken, COMPUTE_AUTH_TOKEN); 57 | verify(mockComputeAuthTokenCache, times(1)).get(eq(COMPUTE_AUTH_TOKEN_CACHE_KEY)); 58 | } 59 | 60 | @Test 61 | public void GIVEN_expiringSoonAuthToken_WHEN_created_THEN_computeAuthToken() throws Exception { 62 | // GIVEN 63 | final GetComputeAuthTokenResponse response = GetComputeAuthTokenResponse.builder() 64 | .authToken(COMPUTE_AUTH_TOKEN) 65 | .fleetId(FLEET_ID) 66 | .computeName(COMPUTE_NAME) 67 | .expirationTimeEpochMillis(Instant.now().plus(Duration.ofMinutes(1))) 68 | .build(); 69 | when(mockComputeAuthTokenCache.get(eq(COMPUTE_AUTH_TOKEN_CACHE_KEY))).thenReturn(response); 70 | 71 | // WHEN 72 | final String authToken = computeAuthTokenManager.getComputeAuthToken(); 73 | 74 | // THEN 75 | assertEquals(authToken, COMPUTE_AUTH_TOKEN); 76 | verify(mockComputeAuthTokenCache, times(2)).get(eq(COMPUTE_AUTH_TOKEN_CACHE_KEY)); 77 | } 78 | 79 | @Test 80 | public void GIVEN_exception_WHEN_cacheGet_THEN_runtimeException() throws Exception { 81 | // GIVEN 82 | doThrow(ExecutionException.class).when(mockComputeAuthTokenCache).get(eq(COMPUTE_AUTH_TOKEN_CACHE_KEY)); 83 | 84 | // WHEN-THEN 85 | assertThrows(RuntimeException.class, () -> computeAuthTokenManager.getComputeAuthToken()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/com/amazon/gamelift/agent/manager/FleetRoleCredentialsConfigurationManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 3 | */ 4 | package com.amazon.gamelift.agent.manager; 5 | 6 | import com.amazon.gamelift.agent.cache.FleetRoleCredentialsCacheLoader; 7 | import com.amazon.gamelift.agent.model.FleetRoleCredentialsConfiguration; 8 | import com.amazon.gamelift.agent.websocket.WebSocketConnectionProvider; 9 | import com.amazonaws.auth.AWSCredentialsProvider; 10 | import com.amazonaws.auth.AWSSessionCredentials; 11 | import com.amazonaws.auth.AWSStaticCredentialsProvider; 12 | import com.amazonaws.auth.BasicSessionCredentials; 13 | import com.google.common.cache.CacheBuilder; 14 | import com.google.common.cache.LoadingCache; 15 | import lombok.extern.slf4j.Slf4j; 16 | 17 | import java.time.Duration; 18 | import java.util.concurrent.ExecutionException; 19 | 20 | @Slf4j 21 | public class FleetRoleCredentialsConfigurationManager { 22 | 23 | private static final String FLEET_ROLE_CREDENTIALS_CONFIGURATION_CACHE_KEY = "fleetRoleCredentialsConfiguration"; 24 | private static final int MAX_CACHE_ENTRIES = 1; 25 | private static final Duration CACHE_EXPIRATION_MINUTES = Duration.ofMinutes(60); 26 | private static final Duration CACHE_REFRESH_MINUTES = Duration.ofMinutes(45); 27 | private static final long CACHE_FORCE_REFRESH_MILLIS = Duration.ofMinutes(1).toMillis(); 28 | 29 | private final LoadingCache fleetRoleCredentialsConfigurationCache; 30 | 31 | /** 32 | * Constructor for FleetRoleCredentialsConfigurationManager 33 | * @param webSocketConnectionProvider 34 | */ 35 | public FleetRoleCredentialsConfigurationManager(final WebSocketConnectionProvider webSocketConnectionProvider) { 36 | this.fleetRoleCredentialsConfigurationCache = CacheBuilder.newBuilder() 37 | .maximumSize(MAX_CACHE_ENTRIES) 38 | .expireAfterWrite(CACHE_EXPIRATION_MINUTES) 39 | .refreshAfterWrite(CACHE_REFRESH_MINUTES) 40 | .recordStats() 41 | .build(new FleetRoleCredentialsCacheLoader(webSocketConnectionProvider)); 42 | } 43 | 44 | /** 45 | * Gets FleetRoleCredentialsConfiguration stored in cache 46 | * @return 47 | */ 48 | public synchronized FleetRoleCredentialsConfiguration getFleetRoleCredentialsConfiguration() { 49 | try { 50 | final FleetRoleCredentialsConfiguration inCache = fleetRoleCredentialsConfigurationCache 51 | .get(FLEET_ROLE_CREDENTIALS_CONFIGURATION_CACHE_KEY); 52 | if (System.currentTimeMillis() + CACHE_FORCE_REFRESH_MILLIS >= inCache.getExpiration()) { 53 | // Force-reload cached credentials are within a minute or passed expired 54 | log.info("Fleet Role credentials are about to expire, forcing refresh."); 55 | fleetRoleCredentialsConfigurationCache.invalidate(FLEET_ROLE_CREDENTIALS_CONFIGURATION_CACHE_KEY); 56 | return fleetRoleCredentialsConfigurationCache.get(FLEET_ROLE_CREDENTIALS_CONFIGURATION_CACHE_KEY); 57 | } 58 | return inCache; 59 | 60 | } catch (final ExecutionException e) { 61 | log.error("Caught an exception while loading FleetRoleCredentials from cache", e); 62 | throw new RuntimeException("Caught an exception while loading FleetRoleCredentials from cache", e); 63 | } 64 | } 65 | 66 | /** 67 | * Retrieve a static credential provider with short-lived credentials 68 | * @return 69 | */ 70 | public AWSCredentialsProvider getFleetRoleCredentials() { 71 | final FleetRoleCredentialsConfiguration configuration = getFleetRoleCredentialsConfiguration(); 72 | final AWSSessionCredentials sessionCredentials = new BasicSessionCredentials( 73 | configuration.getAccessKeyId(), configuration.getSecretAccessKey(), configuration.getSessionToken()); 74 | return new AWSStaticCredentialsProvider(sessionCredentials); 75 | } 76 | } 77 | --------------------------------------------------------------------------------