// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AmazonGameLiftPlugin.Core.DeploymentManagement.Models;
using AmazonGameLiftPlugin.Core.SettingsManagement.Models;
using AmazonGameLiftPlugin.Core.Shared;
using UnityEditor;
using UnityEngine;
using CoreErrorCode = AmazonGameLiftPlugin.Core.Shared.ErrorCode;
namespace AmazonGameLift.Editor
{
///
/// The main backend for deployment to AWS.
///
internal class DeploymentSettings
{
private const int StackInfoRefreshDelayMs = 2000;
private readonly List _deployers = new List();
private readonly ScenarioLocator _scenarioLocator;
private readonly PathConverter _pathConverter;
private readonly CoreApi _coreApi;
private readonly ScenarioParametersUpdater _parametersUpdater;
private readonly TextProvider _textProvider;
private readonly DeploymentWaiter _deploymentWaiter;
private readonly DelayedOperation _delayedStackInfoRefresh;
private int _scenarioIndex;
private string _gameName;
private DeploymentStackInfo _currentStackInfo;
private readonly IDeploymentIdContainer _currentDeploymentId;
private readonly ILogger _logger;
private readonly Status _status = new Status();
public IReadStatus Status => _status;
#region Bootstrap parameters
public string CurrentProfile { get; private set; }
public string CurrentRegion { get; private set; }
public string CurrentBucketName { get; private set; }
public bool HasCurrentBucket { get; private set; }
public bool IsBootstrapped => CurrentProfile != null && HasCurrentBucket;
#endregion
public string[] AllScenarios { get; private set; } = new string[0];
public string ScenarioName { get; private set; }
public string ScenarioPath { get; private set; }
public string ScenarioDescription { get; private set; }
public string ScenarioHelpUrl { get; private set; }
#region Scenario parameters
public string GameName
{
get => _gameName;
set => _ = SetGameNameAsync(value);
}
public string BuildFolderPath { get; set; }
public string BuildFilePath { get; set; }
#endregion Scenario parameters
public int ScenarioIndex
{
get => _scenarioIndex;
set
{
if (_scenarioIndex == value)
{
return;
}
_scenarioIndex = value;
RefreshScenario();
}
}
public bool IsFormFilled => !string.IsNullOrWhiteSpace(GameName)
&& IsValidScenarioIndex
&& IsBuildFolderPathFilled
&& IsBuildFilePathFilled;
public bool IsBuildFilePathFilled => IsValidScenarioIndex
&& (!_deployers[ScenarioIndex].HasGameServer || _coreApi.FileExists(BuildFilePath));
public bool IsBuildFolderPathFilled => IsValidScenarioIndex
&& (!_deployers[ScenarioIndex].HasGameServer || _coreApi.FolderExists(BuildFolderPath));
public bool IsBuildRequired =>
IsValidScenarioIndex && _deployers[ScenarioIndex].HasGameServer;
public bool IsValidScenarioIndex => ScenarioIndex >= 0 && ScenarioIndex < _deployers.Count;
public bool DoesDeploymentExist { get; private set; }
public bool HasCurrentStack => CurrentStackInfo.Details != null;
public DeploymentStackInfo CurrentStackInfo
{
get => _currentStackInfo;
private set
{
_currentStackInfo = value;
CurrentStackInfoChanged?.Invoke();
}
}
public bool IsDeploymentRunning { get; private set; }
public bool CanCancel => _deploymentWaiter.CanCancel == true;
public bool CanDeploy => !IsDeploymentRunning && IsBootstrapped && IsFormFilled && IsCurrentStackModifiable;
public bool IsCurrentStackModifiable =>
CurrentStackInfo.StackStatus == null || CurrentStackInfo.StackStatus.IsStackStatusModifiable();
public event Action CurrentStackInfoChanged;
internal DeploymentSettings(ScenarioLocator scenarioLocator, PathConverter pathConverter,
CoreApi coreApi, ScenarioParametersUpdater parametersUpdater, TextProvider textProvider, DeploymentWaiter deploymentWaiter, IDeploymentIdContainer currentDeploymentId, Delay delay, ILogger logger)
{
_scenarioLocator = scenarioLocator ?? throw new ArgumentNullException(nameof(scenarioLocator));
_pathConverter = pathConverter ?? throw new ArgumentNullException(nameof(pathConverter));
_coreApi = coreApi ?? throw new ArgumentNullException(nameof(coreApi));
_parametersUpdater = parametersUpdater ?? throw new ArgumentNullException(nameof(parametersUpdater));
_textProvider = textProvider ?? throw new ArgumentNullException(nameof(textProvider));
_deploymentWaiter = deploymentWaiter ?? throw new ArgumentNullException(nameof(deploymentWaiter));
_currentDeploymentId = currentDeploymentId ?? throw new ArgumentNullException(nameof(currentDeploymentId));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
ClearCurrentStackInfo();
_delayedStackInfoRefresh = new DelayedOperation(RefreshCurrentStackInfo, delay, StackInfoRefreshDelayMs);
}
public async Task SetGameNameAsync(string value)
{
if (_gameName == value)
{
return;
}
_gameName = value;
ClearCurrentStackInfo();
await _delayedStackInfoRefresh.Request();
}
public void Refresh()
{
IEnumerable deployers = _scenarioLocator.GetScenarios();
_deployers.Clear();
_deployers.AddRange(deployers);
AllScenarios = _deployers.Select(deployer => deployer.DisplayName).ToArray();
GetSettingResponse bucketNameResponse = _coreApi.GetSetting(SettingsKeys.CurrentBucketName);
CurrentBucketName = bucketNameResponse.Success ? bucketNameResponse.Value : null;
GetSettingResponse currentRegionResponse = _coreApi.GetSetting(SettingsKeys.CurrentRegion);
CurrentRegion = currentRegionResponse.Success ? currentRegionResponse.Value : null;
HasCurrentBucket = !string.IsNullOrEmpty(CurrentBucketName) && _coreApi.IsValidRegion(CurrentRegion);
GetSettingResponse profileResponse = _coreApi.GetSetting(SettingsKeys.CurrentProfileName);
CurrentProfile = profileResponse.Success ? profileResponse.Value : null;
RefreshScenario();
}
public void RefreshCurrentStackInfo()
{
if (IsDeploymentRunning)
{
return;
}
if (string.IsNullOrEmpty(GameName))
{
ClearCurrentStackInfo();
return;
}
if (string.IsNullOrEmpty(CurrentProfile))
{
_logger.Log(string.Format(DevStrings.FailedToDescribeStackTemplate, DevStrings.ProfileInvalid), LogType.Warning);
ClearCurrentStackInfo();
return;
}
if (!_coreApi.IsValidRegion(CurrentRegion))
{
_logger.Log(string.Format(DevStrings.FailedToDescribeStackTemplate, DevStrings.RegionInvalid), LogType.Warning);
ClearCurrentStackInfo();
return;
}
string stackName = _coreApi.GetStackName(GameName);
DescribeStackResponse describeResponse = _coreApi.DescribeStack(CurrentProfile, CurrentRegion, stackName);
if (!describeResponse.Success)
{
ClearCurrentStackInfo();
return;
}
CurrentStackInfo = DeploymentStackInfoFactory.Create(_textProvider, describeResponse, CurrentRegion, ScenarioName);
}
public void Restore()
{
ScenarioIndex = 1; // Selects "Single-Region Fleet" Deployment Scenario by default
BuildFilePath = null;
BuildFolderPath = null;
GetSettingResponse response = _coreApi.GetSetting(SettingsKeys.DeploymentGameName);
GameName = response.Success ? response.Value : null;
GetSettingResponse scenarioIndexResponse = _coreApi.GetSetting(SettingsKeys.DeploymentScenarioIndex);
if (scenarioIndexResponse.Success)
{
int? index = SettingsFormatter.ParseInt(scenarioIndexResponse.Value);
ScenarioIndex = index ?? 0;
}
GetSettingResponse serverPathResponse = _coreApi.GetSetting(SettingsKeys.DeploymentBuildFolderPath);
if (serverPathResponse.Success)
{
BuildFolderPath = serverPathResponse.Value;
}
GetSettingResponse serverExePathResponse = _coreApi.GetSetting(SettingsKeys.DeploymentBuildFilePath);
if (serverExePathResponse.Success)
{
BuildFilePath = serverExePathResponse.Value;
}
}
public void Save()
{
_coreApi.PutSetting(SettingsKeys.DeploymentScenarioIndex, SettingsFormatter.FormatInt(ScenarioIndex));
_coreApi.PutSettingOrClear(SettingsKeys.DeploymentBuildFolderPath, BuildFolderPath);
_coreApi.PutSettingOrClear(SettingsKeys.DeploymentBuildFilePath, BuildFilePath);
_coreApi.PutSettingOrClear(SettingsKeys.DeploymentGameName, GameName);
}
public async Task WaitForCurrentDeployment()
{
if (IsDeploymentRunning || !_currentDeploymentId.HasValue)
{
return;
}
try
{
IsDeploymentRunning = true;
_delayedStackInfoRefresh.Cancel();
_deploymentWaiter.InfoUpdated += OnDeploymentWaiterInfoUpdated;
DeploymentResponse response = await _deploymentWaiter.WaitUntilDone(_currentDeploymentId.Get());
LogWaitResponse(response);
if (response.ErrorCode != ErrorCode.OperationCancelled)
{
_currentDeploymentId.Clear();
}
}
catch (Exception)
{
_currentDeploymentId.Clear();
CurrentStackInfo = new DeploymentStackInfo(_textProvider.Get(Strings.StatusExceptionThrown));
throw;
}
finally
{
IsDeploymentRunning = false;
_deploymentWaiter.InfoUpdated -= OnDeploymentWaiterInfoUpdated;
}
}
private void LogWaitResponse(DeploymentResponse response)
{
if (response.Success || response.ErrorCode == ErrorCode.OperationCancelled)
{
return;
}
if (response.ErrorCode == ErrorCode.StackStatusInvalid
|| response.ErrorCode == CoreErrorCode.StackDoesNotExist)
{
_logger.LogResponseError(response, LogType.Log);
}
else
{
_logger.LogResponseError(response);
}
}
public void CancelWaitingForDeployment()
{
_deploymentWaiter.CancelWaiting();
}
public void CancelDeployment()
{
if (!CanCancel)
{
_logger.Log(DevStrings.OperationInvalid, LogType.Warning);
return;
}
Response response = _deploymentWaiter.CancelDeployment();
if (response.Success)
{
RefreshCurrentStackInfo();
}
else
{
_logger.LogResponseError(response);
}
}
public async Task StartDeployment(ConfirmChangesDelegate confirmChanges)
{
if (confirmChanges is null)
{
throw new ArgumentNullException(nameof(confirmChanges));
}
if (!IsFormFilled)
{
return;
}
string exeFilePath = null;
DeployerBase currentDeployer = _deployers[ScenarioIndex];
if (currentDeployer.HasGameServer)
{
exeFilePath = GetExeFilePathInBuildOrNull();
if (exeFilePath == null)
{
_status.IsDisplayed = true;
_status.SetMessage(_textProvider.Get(Strings.StatusDeploymentExePathInvalid), MessageType.Error);
return;
}
}
if (IsDeploymentRunning)
{
return;
}
IsDeploymentRunning = true;
_delayedStackInfoRefresh.Cancel();
string parametersPath = _pathConverter.GetParametersFilePath(ScenarioPath);
IReadOnlyDictionary parameters = currentDeployer.HasGameServer
? PrepareParameters(exeFilePath)
: PrepareGameParameter();
_parametersUpdater.Update(parametersPath, parameters);
CurrentStackInfo = new DeploymentStackInfo(_textProvider.Get(Strings.StatusDeploymentStarting));
string stackName = _coreApi.GetStackName(GameName);
var deploymentId = new DeploymentId(CurrentProfile, CurrentRegion, stackName, currentDeployer.DisplayName);
_currentDeploymentId.Set(deploymentId);
try
{
DeploymentResponse response = await currentDeployer.StartDeployment(ScenarioPath, BuildFolderPath, GameName,
isDevelopmentBuild: EditorUserBuildSettings.development, confirmChanges);
if (!response.Success)
{
if (response.ErrorCode != ErrorCode.OperationCancelled)
{
_logger.LogResponseError(response);
string messageTemplate = _textProvider.Get(Strings.StatusDeploymentFailure);
string message = string.Format(messageTemplate, _textProvider.GetError(response.ErrorCode));
_status.SetMessage(message, MessageType.Error);
_status.IsDisplayed = true;
}
return;
}
_deploymentWaiter.InfoUpdated += OnDeploymentWaiterInfoUpdated;
response = await _deploymentWaiter.WaitUntilDone(deploymentId);
LogWaitResponse(response);
if (response.ErrorCode != ErrorCode.OperationCancelled)
{
_currentDeploymentId.Clear();
}
}
catch (Exception ex)
{
_currentDeploymentId.Clear();
_logger.LogException(ex);
string messageTemplate = _textProvider.Get(Strings.StatusDeploymentFailure);
string message = string.Format(messageTemplate, ex.Message);
_status.SetMessage(message, MessageType.Error);
_status.IsDisplayed = true;
throw;
}
finally
{
IsDeploymentRunning = false;
_deploymentWaiter.InfoUpdated -= OnDeploymentWaiterInfoUpdated;
RefreshCurrentStackInfo();
}
}
private void OnDeploymentWaiterInfoUpdated(DeploymentInfo info)
{
CurrentStackInfo = DeploymentStackInfoFactory.Create(_textProvider, info);
}
private IReadOnlyDictionary PrepareGameParameter()
{
return new Dictionary
{
{ ScenarioParameterKeys.GameName, GameName }
};
}
private IReadOnlyDictionary PrepareParameters(string exeFilePathInBuild)
{
string launchPath = _coreApi.GetServerGamePath(exeFilePathInBuild);
return new Dictionary
{
{ ScenarioParameterKeys.GameName, GameName },
{ ScenarioParameterKeys.LaunchPath, launchPath },
};
}
private string GetExeFilePathInBuildOrNull()
{
int index = BuildFilePath.IndexOf(BuildFolderPath);
if (index < 0)
{
return null;
}
return BuildFilePath.Remove(0, BuildFolderPath.Length).TrimStart('\\', '/');
}
private void RefreshScenario()
{
if (!IsValidScenarioIndex)
{
return;
}
DeployerBase deployer = _deployers[ScenarioIndex];
ScenarioName = deployer.DisplayName;
ScenarioDescription = deployer.Description;
ScenarioHelpUrl = deployer.HelpUrl;
ScenarioPath = _pathConverter.GetScenarioAbsolutePath(deployer.ScenarioFolder);
if (!deployer.HasGameServer)
{
BuildFolderPath = null;
BuildFilePath = null;
}
RefreshCurrentStackInfo();
}
private void ClearCurrentStackInfo()
{
CurrentStackInfo = new DeploymentStackInfo(_textProvider.Get(Strings.StatusNothingDeployed));
}
}
}