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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace GameLift {
namespace Internal {
namespace Test {

static const std::string sdkVersion = "5.5.0";
static const std::string sdkVersion = "5.6.0";
// private constants copied over for testing purposes
static constexpr const char *PID_KEY = "pID";
static constexpr const char *SDK_VERSION_KEY = "sdkVersion";
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <aws/gamelift/internal/GameLiftServerState.h>
#include <aws/gamelift/internal/util/LoggerHelper.h>
#include <aws/gamelift/server/CustomLoggerConfiguration.h>
#include <spdlog/spdlog.h>
#include <iostream>

namespace Aws {
namespace GameLift {
namespace Server {
namespace Test {

static const std::string sdkVersion = "5.5.0";
static const std::string sdkVersion = "5.6.0";

TEST(GameLiftServerAPITest, GIVEN_SdkVersion_WHEN_GetSdkVersion_THEN_success) {
// GIVEN
Expand All @@ -35,6 +38,55 @@ TEST(GameLiftServerAPITest, GIVEN_SdkVersion_WHEN_GetSdkVersion_THEN_success) {
#endif
}

TEST(GameLiftServerAPITest, GIVEN_nullCallback_WHEN_initCustomLogger_THEN_returnsBadRequest) {
// Server::InitCustomLogger rejects null callbacks to prevent crashes on the first log message.
// GIVEN
Aws::GameLift::Server::CustomLoggerConfiguration params(nullptr, nullptr, Aws::GameLift::Server::LogLevel::Info);

// WHEN
GenericOutcome outcome = Server::InitCustomLogger(params);

// THEN
EXPECT_FALSE(outcome.IsSuccess());
EXPECT_EQ(outcome.GetError().GetErrorType(), GAMELIFT_ERROR_TYPE::BAD_REQUEST_EXCEPTION);
}

namespace {
void TestLogCallback(Aws::GameLift::Server::LogLevel level, const char* message, void* userData) {
auto* counter = static_cast<int*>(userData);
++(*counter);
}
} // anonymous namespace

TEST(GameLiftServerAPITest, GIVEN_customLoggerAlreadyRegistered_WHEN_initCustomLoggerCalledAgain_THEN_returnsAlreadyInitialized) {
// Ensure clean state for this test.
Aws::GameLift::Internal::LoggerHelper::ResetCustomLoggerRegistered();

// GIVEN - first InitCustomLogger with a valid callback succeeds
int callCountA = 0;
Aws::GameLift::Server::CustomLoggerConfiguration paramsA(TestLogCallback, &callCountA, Aws::GameLift::Server::LogLevel::Info);
GenericOutcome firstOutcome = Server::InitCustomLogger(paramsA);
ASSERT_TRUE(firstOutcome.IsSuccess());

// WHEN - second InitCustomLogger with a different valid callback
int callCountB = 0;
Aws::GameLift::Server::CustomLoggerConfiguration paramsB(TestLogCallback, &callCountB, Aws::GameLift::Server::LogLevel::Warn);
GenericOutcome secondOutcome = Server::InitCustomLogger(paramsB);

// THEN - returns ALREADY_INITIALIZED
EXPECT_FALSE(secondOutcome.IsSuccess());
EXPECT_EQ(secondOutcome.GetError().GetErrorType(), GAMELIFT_ERROR_TYPE::ALREADY_INITIALIZED);

// The original callback remains active (second callback was never registered)
spdlog::info("verify original callback still active");
spdlog::default_logger()->flush();
EXPECT_GT(callCountA, 0);
EXPECT_EQ(callCountB, 0);

// Cleanup
Aws::GameLift::Internal::LoggerHelper::ResetCustomLoggerRegistered();
}

} // namespace Test
} // namespace Server
} // namespace GameLift
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ class WebSocketppClientWrapper : public IWebSocketClientWrapper {

private:
const int WEBSOCKET_OPEN_HANDSHAKE_TIMEOUT_MILLIS = 20000; // 20 seconds
// Hard upper bound on how long PerformConnect will block waiting for a connection to
// open or fail. Backstop for cases where neither OnConnected nor OnError ever fires
// (e.g. a stalled TCP/TLS handshake that the websocketpp open-handshake timeout does
// not cover). Must be >= WEBSOCKET_OPEN_HANDSHAKE_TIMEOUT_MILLIS so a legitimately
// in-progress handshake can still complete before we give up on the attempt.
const int CONNECT_WAIT_TIMEOUT_MILLIS = WEBSOCKET_OPEN_HANDSHAKE_TIMEOUT_MILLIS + 5000; // 25 seconds
const int SERVICE_CALL_TIMEOUT_MILLIS = 20000; // 20 seconds
const int OK_STATUS_CODE = 200;
const int WAIT_FOR_RECONNECT_RETRY_DELAY_SECONDS = 5;
Expand All @@ -55,6 +61,11 @@ class WebSocketppClientWrapper : public IWebSocketClientWrapper {
// The WebSocketpp objects this class wraps
std::shared_ptr<WebSocketppClientType> m_webSocketClient;
WebSocketppClientType::connection_ptr m_connection;
// The connection PerformConnect is currently awaiting. Used so OnConnected/OnError can
// ignore stale callbacks from a connection we have already abandoned after a wait
// timeout (otherwise a late callback could falsely satisfy a later attempt's wait).
// Guarded by m_lock.
WebSocketppClientType::connection_ptr m_pendingConnection;
std::unique_ptr<std::thread> m_socket_thread_1;
std::unique_ptr<std::thread> m_socket_thread_2;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,66 @@
*/
#pragma once

#include <aws/gamelift/common/Outcome.h>
#include <aws/gamelift/server/CustomLoggerConfiguration.h>
#include <string>

namespace Aws {
namespace GameLift {
namespace Internal {

/**
* Internal helper that manages SDK logging configuration via sink-level indirection.
*
* Design: The default spdlog logger ("multi_sink") is created once and never reassigned
* during the SDK's lifetime. Its sole sink is a dist_sink_mt, which wraps child sinks
* (stdout+file for default, or a CallbackSink for custom logging). When InitCustomLogger() is
* called after InitSDK(), the dist_sink's children are atomically swapped via set_sinks()
* — no spdlog::drop() or set_default_logger() occurs in the hot path, eliminating the
* data race on spdlog's unsynchronized default_logger_raw() pointer.
*
* Thread safety: dist_sink_mt::set_sinks() and dist_sink_mt::log() both acquire the same
* base_sink<std::mutex>::mutex_ (vendored spdlog v1.14.0, dist_sink.h:41, base_sink-inl.h:27),
* so they are mutually exclusive. Logger::set_level() is safe because level_ is
* std::atomic<int> (common.h:228).
*
* Call ordering:
* - InitCustomLogger before InitSDK: callback logger created with dist_sink wrapping CallbackSink.
* Subsequent InitializeLogger(processId) from InitSDK is a no-op (CAS guard check).
* - InitSDK before InitCustomLogger: default logger created with dist_sink wrapping [stdout, file].
* InitializeCallbackLogger swaps children to [CallbackSink] via set_sinks().
* - InitCustomLogger can be called at most once; subsequent calls return ALREADY_INITIALIZED.
*/
class LoggerHelper {
#ifdef GAMELIFT_USE_STD
public:
static void InitializeLogger(const std::string& process_Id);
/// Registers a custom callback logger. Safe to call before or after InitSDK().
/// After InitSDK(), background threads may be actively logging; the sink swap is
/// serialized with in-flight log calls via the dist_sink_mt mutex.
static GenericOutcome InitializeCallbackLogger(const Aws::GameLift::Server::CustomLoggerConfiguration& logParameters);
static bool IsCustomLoggerRegistered();
static void ResetCustomLoggerRegistered();

/// Drops the SDK logger from spdlog's registry. Encapsulates the logger name
/// so callers don't need to hard-code the "multi_sink" literal.
static void DropSdkLogger();

#ifdef GAMELIFT_USE_STD
static GenericOutcome InitializeLogger(const std::string& process_Id);
/// Initializes the logger using logParameters. If logParameters.callback is null, this
/// overload treats it as "no custom logger configured" and falls back to the default file
/// logger — unlike the public Server::InitCustomLogger() API, which rejects null with
/// BAD_REQUEST_EXCEPTION because it represents explicit user intent.
static GenericOutcome InitializeLogger(const std::string& process_Id, const Aws::GameLift::Server::CustomLoggerConfiguration& logParameters);
#else
public:
static void InitializeLogger(const char* process_Id);
static GenericOutcome InitializeLogger(const char* process_Id);
/// Initializes the logger using logParameters. If logParameters.callback is null, this
/// overload treats it as "no custom logger configured" and falls back to the default file
/// logger — unlike the public Server::InitCustomLogger() API, which rejects null with
/// BAD_REQUEST_EXCEPTION because it represents explicit user intent.
static GenericOutcome InitializeLogger(const char* process_Id, const Aws::GameLift::Server::CustomLoggerConfiguration& logParameters);
#endif
};

} // namespace Internal
} // namespace GameLift
} // namespace Aws
} // namespace Aws
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once

#include <aws/gamelift/common/GameLift_EXPORTS.h>

namespace Aws {
namespace GameLift {
namespace Server {

/**
* Log levels for the custom logging callback.
* Values are ordered by increasing severity. Trace is the most verbose;
* Fatal is the most severe. Off disables all callback dispatching.
*/
enum class LogLevel {
Trace = 0,
Debug = 1,
Info = 2,
Warn = 3,
Error = 4,
Fatal = 5,
// Values 6–99 reserved for future severity levels.
Off = 100 ///< Disables all log callback dispatching
};

/**
* Callback function type for custom logging.
* @param level The severity level of the log message.
* @param message The pre-formatted log message string (null-terminated).
* The pointer is only valid for the duration of the callback invocation.
* Callers that need the message after the callback returns must copy it
* (e.g., into a std::string or a queue buffer).
* The message length is unbounded -- messages may embed full service
* payloads (e.g., game session data or matchmaker data) and can be
* arbitrarily large. Do not copy the message into fixed-size buffers
* without bounds checking.
* @param userData The user-provided context pointer passed in CustomLoggerConfiguration.
*
* Thread safety: The SDK serializes callback invocations via an internal mutex
* (the callback will not be invoked from multiple threads concurrently), but it
* may be called from any thread. Implementations do not need their own
* synchronization unless they access shared external state.
*
* This serialization guarantee holds only while the SDK is running normally.
* Teardown is NOT fully synchronized: the game-session, process-terminate, and
* update-game-session handlers run on detached threads that Destroy() does not
* join, so a callback may still be invoked while Destroy() runs or shortly after
* it returns. There is therefore no point before process exit at which the SDK
* can guarantee no callback is in flight. Keep the callback, its module (DLL/
* shared library), and userData valid until the process exits; do not free or
* unload them after Destroy() returns. Likewise, avoid retaining your own
* shared_ptr to the SDK logger across Destroy() -- logging through a retained
* reference may still invoke the callback.
*
* @note The callback is invoked synchronously on the logging thread.
* Long-running operations (network I/O, file writes, etc.) will block
* all SDK logging until the callback returns. Consider queuing messages
* for asynchronous processing if your logging backend involves I/O.
*
* @note The callback pointer, its module, and userData must remain valid until
* process exit -- not merely until Destroy() returns. See the thread
* safety notes above.
*/
using LogCallback = void(*)(LogLevel level, const char* message, void* userData);

/**
* Parameters for configuring a custom logger.
* When a LogCallback is provided, it replaces the default stdout and file logging.
* When no LogCallback is provided (nullptr), the SDK uses its default logging behavior.
*
* @note Log configuration is immutable after InitSDK() is called. The log level
* and callback cannot be changed at runtime. To change logging behavior,
* the process must be restarted.
*/
struct AWS_GAMELIFT_API CustomLoggerConfiguration {
/** Custom log callback. Set to nullptr for default SDK logging behavior. */
LogCallback callback = nullptr;

/** User-provided context pointer passed to the callback.
* The SDK does not manage its lifetime.
*
* Must remain valid from InitSDK() until the process terminates -- not merely
* until Destroy() returns. See the thread safety notes on LogCallback above.
*
* @code
* Server::Destroy();
* // Do NOT delete myLogContext here: a detached callback thread may still be
* // running and dereference it. Let the OS reclaim it at process exit, or
* // otherwise guarantee no callback can run before freeing it.
* @endcode
*/
void* userData = nullptr;

/** Minimum log level. Messages below this level are not dispatched. */
LogLevel minimumLogLevel = LogLevel::Info;

CustomLoggerConfiguration() = default;

explicit CustomLoggerConfiguration(LogCallback cb, void* ud = nullptr, LogLevel minLevel = LogLevel::Info)
: callback(cb), userData(ud), minimumLogLevel(minLevel) {}
};

} // namespace Server
} // namespace GameLift
} // namespace Aws
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

#include <aws/gamelift/server/ProcessParameters.h>
#include <aws/gamelift/server/MetricsParameters.h>
#include <aws/gamelift/server/CustomLoggerConfiguration.h>
#include <aws/gamelift/server/model/DescribePlayerSessionsRequest.h>
#include <aws/gamelift/server/model/GetFleetRoleCredentialsRequest.h>
#include <aws/gamelift/server/model/ServerParameters.h>
Expand Down Expand Up @@ -81,6 +82,25 @@ Uses the provided parameters exactly as specified. To use environment variables
*/
AWS_GAMELIFT_API GenericOutcome InitMetrics(const Aws::GameLift::Server::MetricsParameters &metricsParameters);

/**
Initializes custom logging with the specified callback parameters.
For best results, call InitCustomLogger() BEFORE InitSDK() so that SDK initialization
diagnostics (connection setup, logger init, InitSDK errors) are routed to your
custom log callback from the very start.

Calling InitCustomLogger() after InitSDK() is also safe — the switch is thread-safe with
respect to SDK background threads, and log output switches to the callback from that
point onward; messages logged before the call go to the default file/console logger.

This method can be called at most once; subsequent calls return ALREADY_INITIALIZED
and do NOT change the existing logger. A null callback is rejected with
BAD_REQUEST_EXCEPTION.

@param logParameters Parameters for configuring custom logging (callback, userData, minimumLogLevel).
@return GenericOutcome indicating success or failure.
*/
AWS_GAMELIFT_API GenericOutcome InitCustomLogger(const Aws::GameLift::Server::CustomLoggerConfiguration &logParameters);

/**
Signals Amazon GameLift Servers that the process is ready to receive GameSessions.
The onStartGameSession callback will be invoked when the server is bound to a GameSession.
Expand Down Expand Up @@ -209,6 +229,25 @@ Uses the provided parameters exactly as specified. To use environment variables
*/
AWS_GAMELIFT_API GenericOutcome InitMetrics(const Aws::GameLift::Server::MetricsParameters &metricsParameters);

/**
Initializes custom logging with the specified callback parameters.
For best results, call InitCustomLogger() BEFORE InitSDK() so that SDK initialization
diagnostics (connection setup, logger init, InitSDK errors) are routed to your
custom log callback from the very start.

Calling InitCustomLogger() after InitSDK() is also safe — the switch is thread-safe with
respect to SDK background threads, and log output switches to the callback from that
point onward; messages logged before the call go to the default file/console logger.

This method can be called at most once; subsequent calls return ALREADY_INITIALIZED
and do NOT change the existing logger. A null callback is rejected with
BAD_REQUEST_EXCEPTION.

@param logParameters Parameters for configuring custom logging (callback, userData, minimumLogLevel).
@return GenericOutcome indicating success or failure.
*/
AWS_GAMELIFT_API GenericOutcome InitCustomLogger(const Aws::GameLift::Server::CustomLoggerConfiguration &logParameters);

/**
Signals Amazon GameLift Servers that the process is ready to receive GameSessions.
The onStartGameSession callback will be invoked when the server is bound to a GameSession.
Expand Down
Loading