diff --git a/.gitignore b/.gitignore index 1f8408e23..1b8a70714 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ UserSettings/Search.settings # ===================================== # Database.sqlite Database/ +Builds/ # ===================================== # # Visual Studio / MonoDevelop / Rider # diff --git a/Assets/Mirror/Hosting.meta b/Assets/Mirror/Hosting.meta new file mode 100644 index 000000000..43de0c569 --- /dev/null +++ b/Assets/Mirror/Hosting.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b13bce90dfb604c2d9170e3640f59ad9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap.meta b/Assets/Mirror/Hosting/Edgegap.meta new file mode 100755 index 000000000..c4f5fd2c1 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b7c51dc3e45095f4a8a960150837fe7b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor.meta b/Assets/Mirror/Hosting/Edgegap/Editor.meta new file mode 100755 index 000000000..e5593273f --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 635b395f47dc9f742b4d71144921bb0d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs new file mode 100755 index 000000000..8bad9d62d --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using UnityEditor; +using UnityEditor.Build.Reporting; + +using Debug = UnityEngine.Debug; + +namespace Edgegap +{ + internal static class EdgegapBuildUtils + { + + public static BuildReport BuildServer() + { + IEnumerable scenes = EditorBuildSettings.scenes.Select(s=>s.path); + BuildPlayerOptions options = new BuildPlayerOptions + { + scenes = scenes.ToArray(), + target = BuildTarget.StandaloneLinux64, + #pragma warning disable CS0618 // disable deprecated warning until Edgegap updates this + options = BuildOptions.EnableHeadlessMode, + #pragma warning restore CS0618 + locationPathName = "Builds/EdgegapServer/ServerBuild" + }; + + return BuildPipeline.BuildPlayer(options); + } + + public static async Task DockerSetupAndInstallationCheck() + { + if (!File.Exists("Dockerfile")) + { + File.WriteAllText("Dockerfile", dockerFileText); + } + + string output = null; + string error = null; + await RunCommand_DockerVersion(msg => output = msg, msg => error = msg); // MIRROR CHANGE + if (!string.IsNullOrEmpty(error)) + { + Debug.LogError(error); + return false; + } + Debug.Log($"[Edgegap] Docker version detected: {output}"); // MIRROR CHANGE + return true; + } + + // MIRROR CHANGE + static async Task RunCommand_DockerVersion(Action outputReciever = null, Action errorReciever = null) + { +#if UNITY_EDITOR_WIN + await RunCommand("cmd.exe", "/c docker --version", outputReciever, errorReciever); +#elif UNITY_EDITOR_OSX + await RunCommand("/bin/bash", "-c \"docker --version\"", outputReciever, errorReciever); +#elif UNITY_EDITOR_LINUX + await RunCommand("/bin/bash", "-c \"docker --version\"", outputReciever, errorReciever); +#else + Debug.LogError("The platform is not supported yet."); +#endif + } + + // MIRROR CHANGE + public static async Task RunCommand_DockerBuild(string registry, string imageRepo, string tag, Action onStatusUpdate) + { + string realErrorMessage = null; + +#if UNITY_EDITOR_WIN + await RunCommand("docker.exe", $"build -t {registry}/{imageRepo}:{tag} .", onStatusUpdate, +#elif UNITY_EDITOR_OSX + await RunCommand("/bin/bash", $"-c \"docker build -t {registry}/{imageRepo}:{tag} .\"", onStatusUpdate, +#elif UNITY_EDITOR_LINUX + await RunCommand("/bin/bash", $"-c \"docker build -t {registry}/{imageRepo}:{tag} .\"", onStatusUpdate, +#endif + (msg) => + { + if (msg.Contains("ERROR")) + { + realErrorMessage = msg; + } + onStatusUpdate(msg); + }); + + if(realErrorMessage != null) + { + throw new Exception(realErrorMessage); + } + } + + public static async Task<(bool, string)> RunCommand_DockerPush(string registry, string imageRepo, string tag, Action onStatusUpdate) + { + string error = string.Empty; +#if UNITY_EDITOR_WIN + await RunCommand("docker.exe", $"push {registry}/{imageRepo}:{tag}", onStatusUpdate, (msg) => error += msg + "\n"); +#elif UNITY_EDITOR_OSX + await RunCommand("/bin/bash", $"-c \"docker push {registry}/{imageRepo}:{tag}\"", onStatusUpdate, (msg) => error += msg + "\n"); +#elif UNITY_EDITOR_LINUX + await RunCommand("/bin/bash", $"-c \"docker push {registry}/{imageRepo}:{tag}\"", onStatusUpdate, (msg) => error += msg + "\n"); +#endif + if (!string.IsNullOrEmpty(error)) + { + Debug.LogError(error); + return (false, error); + } + return (true, null); + } + // END MIRROR CHANGE + + static async Task RunCommand(string command, string arguments, Action outputReciever = null, Action errorReciever = null) + { + ProcessStartInfo startInfo = new ProcessStartInfo() + { + FileName = command, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + // MIRROR CHANGE +#if !UNITY_EDITOR_WIN + // on mac, commands like 'docker' aren't found because it's not in the application's PATH + // even if it runs on mac's terminal. + // to solve this we need to do two steps: + // 1. add /usr/bin/local to PATH if it's not there already. often this is missing in the application. + // this is where docker is usually instaled. + // 2. add PATH to ProcessStartInfo + string existingPath = Environment.GetEnvironmentVariable("PATH"); + string customPath = $"{existingPath}:/usr/local/bin"; + startInfo.EnvironmentVariables["PATH"] = customPath; + // Debug.Log("PATH: " + customPath); +#endif + // END MIRROR CHANGE + + Process proc = new Process() { StartInfo = startInfo, }; + proc.EnableRaisingEvents = true; + + ConcurrentQueue errors = new ConcurrentQueue(); + ConcurrentQueue outputs = new ConcurrentQueue(); + + void PipeQueue(ConcurrentQueue q, Action opt) + { + while (!q.IsEmpty) + { + if (q.TryDequeue(out string msg) && !string.IsNullOrWhiteSpace(msg)) + { + opt?.Invoke(msg); + } + } + } + + proc.OutputDataReceived += (s, e) => outputs.Enqueue(e.Data); + proc.ErrorDataReceived += (s, e) => errors.Enqueue(e.Data); + + proc.Start(); + proc.BeginOutputReadLine(); + proc.BeginErrorReadLine(); + + while (!proc.HasExited) + { + await Task.Delay(100); + PipeQueue(errors, errorReciever); + PipeQueue(outputs, outputReciever); + } + + PipeQueue(errors, errorReciever); + PipeQueue(outputs, outputReciever); + } + + static void Proc_OutputDataReceived(object sender, DataReceivedEventArgs e) + { + throw new NotImplementedException(); + } + + static Regex lastDigitsRegex = new Regex("([0-9])+$"); + + public static string IncrementTag(string tag) + { + Match lastDigits = lastDigitsRegex.Match(tag); + if (!lastDigits.Success) + { + return tag + " _1"; + } + + int number = int.Parse(lastDigits.Groups[0].Value); + + number++; + + return lastDigitsRegex.Replace(tag, number.ToString()); + } + + public static void UpdateEdgegapAppTag(string tag) + { + // throw new NotImplementedException(); + } + + static string dockerFileText = @"FROM ubuntu:bionic + +ARG DEBIAN_FRONTEND=noninteractive + +COPY Builds/EdgegapServer /root/build/ + +WORKDIR /root/ + +RUN chmod +x /root/build/ServerBuild + +ENTRYPOINT [ ""/root/build/ServerBuild"", ""-batchmode"", ""-nographics""] +"; + + + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs.meta new file mode 100755 index 000000000..03529c425 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 97dadab2a073d8b47bf9a270401f0a8f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapScriptEditor.cs b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapScriptEditor.cs new file mode 100755 index 000000000..8b840e95b --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapScriptEditor.cs @@ -0,0 +1,27 @@ +using UnityEditor; +using UnityEngine.UIElements; + +namespace Edgegap +{ + [CustomEditor(typeof(EdgegapToolScript))] + public class EdgegapPluginScriptEditor : Editor + { + VisualElement _serverDataContainer; + + void OnEnable() + { + _serverDataContainer = EdgegapServerDataManager.GetServerDataVisualTree(); + EdgegapServerDataManager.RegisterServerDataContainer(_serverDataContainer); + } + + void OnDisable() + { + EdgegapServerDataManager.DeregisterServerDataContainer(_serverDataContainer); + } + + public override VisualElement CreateInspectorGUI() + { + return _serverDataContainer; + } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapScriptEditor.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapScriptEditor.cs.meta new file mode 100755 index 000000000..60f708f6b --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapScriptEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c4c676ae6dcca0e458c6a8f06571f8fc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerData.uss b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerData.uss new file mode 100755 index 000000000..15cb5e392 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerData.uss @@ -0,0 +1,81 @@ +.row__port-table { + padding: 2px 4px; + display: flex; + flex-direction: row; + align-items: center; + width: auto; + justify-content: space-around; + -unity-text-align: middle-left; +} + + .row__port-table > * { + width: 0px; + flex-grow: 1; + align-self: center; + margin: 0px; + padding: 0px; + } + +.focusable:hover { + background-color: rgba(0,0,0,0.2); + border-radius: 3px; +} + +.row__dns { + display: flex; + flex-direction: row; + align-items: center; + width: auto; + justify-content: space-between; + -unity-text-align: middle-left; +} + +.row__status { + display: flex; + flex-direction: row; + align-items: center; + width: auto; + justify-content: space-between; + -unity-text-align: middle-left; +} + +.label__header { + -unity-font-style: bold +} + +.label__status { + -unity-font-style: bold; + border-radius: 2px; + width: 100px; + color: #fff; + -unity-text-align: middle-center; +} + +.label__info-text { + padding: 8px; + margin: 4px; + border-radius: 3px; + -unity-text-align: middle-center; + white-space: normal; + background-color: rgba(42, 42, 42, 0.5); +} + +.container { + margin: 8px 0px; +} + +.bg--secondary { + background-color: #8a8a8a; +} + +.bg--success { + background-color: #90be6d; +} + +.bg--danger { + background-color: #f94144; +} + +.bg--warning { + background-color: #f9c74f; +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerData.uss.meta b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerData.uss.meta new file mode 100755 index 000000000..b9cce488c --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerData.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: da5e3f58bd8cde14789f7c61df3f59f4 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerDataManager.cs b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerDataManager.cs new file mode 100755 index 000000000..4eebb877d --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerDataManager.cs @@ -0,0 +1,242 @@ +using IO.Swagger.Model; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace Edgegap +{ + static class EdgegapServerDataManagerUtils + { + public static Label GetHeader(string text) + { + Label header = new Label(text); + header.AddToClassList("label__header"); + + return header; + } + + public static VisualElement GetHeaderRow() + { + VisualElement row = new VisualElement(); + row.AddToClassList("row__port-table"); + row.AddToClassList("label__header"); + + row.Add(new Label("Name")); + row.Add(new Label("External")); + row.Add(new Label("Internal")); + row.Add(new Label("Protocol")); + row.Add(new Label("Link")); + + return row; + } + + public static VisualElement GetRowFromPortResponse(PortMapping port) + { + VisualElement row = new VisualElement(); + row.AddToClassList("row__port-table"); + row.AddToClassList("focusable"); + + + row.Add(new Label(port.Name)); + row.Add(new Label(port.External.ToString())); + row.Add(new Label(port.Internal.ToString())); + row.Add(new Label(port.Protocol)); + row.Add(GetCopyButton("Copy", port.Link)); + + return row; + } + + public static Button GetCopyButton(string btnText, string copiedText) + { + Button copyBtn = new Button(); + copyBtn.text = btnText; + copyBtn.clickable.clicked += () => GUIUtility.systemCopyBuffer = copiedText; + + return copyBtn; + } + + public static Button GetLinkButton(string btnText, string targetUrl) + { + Button copyBtn = new Button(); + copyBtn.text = btnText; + copyBtn.clickable.clicked += () => UnityEngine.Application.OpenURL(targetUrl); + + return copyBtn; + } + public static Label GetInfoText(string innerText) + { + Label infoText = new Label(innerText); + infoText.AddToClassList("label__info-text"); + + return infoText; + } + } + + /// + /// Utility class to centrally manage the Edgegap server data, and create / update the elements displaying the server info. + /// + public static class EdgegapServerDataManager + { + static Status _serverData; + static ApiEnvironment _apiEnvironment; + + // UI elements + static readonly StyleSheet _serverDataStylesheet; + static readonly List _serverDataContainers = new List(); + + public static Status GetServerStatus() => _serverData; + + static EdgegapServerDataManager() + { + // MIRROR CHANGE + _serverDataStylesheet = AssetDatabase.LoadAssetAtPath($"{EdgegapWindow.StylesheetPath}/EdgegapServerData.uss"); + // END MIRROR CHANGE + } + public static void RegisterServerDataContainer(VisualElement serverDataContainer) + { + _serverDataContainers.Add(serverDataContainer); + } + public static void DeregisterServerDataContainer(VisualElement serverDataContainer) + { + _serverDataContainers.Remove(serverDataContainer); + } + public static void SetServerData(Status serverData, ApiEnvironment apiEnvironment) + { + _serverData = serverData; + _apiEnvironment = apiEnvironment; + RefreshServerDataContainers(); + } + + static VisualElement GetStatusSection() + { + ServerStatus serverStatus = _serverData.GetServerStatus(); + string dashboardUrl = _apiEnvironment.GetDashboardUrl(); + string requestId = _serverData.RequestId; + string deploymentDashboardUrl = ""; + + if (!string.IsNullOrEmpty(requestId) && !string.IsNullOrEmpty(dashboardUrl)) + { + deploymentDashboardUrl = $"{dashboardUrl}/arbitrium/deployment/read/{requestId}/"; + } + + VisualElement container = new VisualElement(); + container.AddToClassList("container"); + + container.Add(EdgegapServerDataManagerUtils.GetHeader("Server Status")); + + VisualElement row = new VisualElement(); + row.AddToClassList("row__status"); + + // Status pill + Label statusLabel = new Label(serverStatus.GetLabelText()); + statusLabel.AddToClassList(serverStatus.GetStatusBgClass()); + statusLabel.AddToClassList("label__status"); + row.Add(statusLabel); + + // Link to dashboard + if (!string.IsNullOrEmpty(deploymentDashboardUrl)) + { + row.Add(EdgegapServerDataManagerUtils.GetLinkButton("See in the dashboard", deploymentDashboardUrl)); + } + else + { + row.Add(new Label("Could not resolve link to this deployment")); + } + + container.Add(row); + + return container; + } + + static VisualElement GetDnsSection() + { + string serverDns = _serverData.Fqdn; + + VisualElement container = new VisualElement(); + container.AddToClassList("container"); + + container.Add(EdgegapServerDataManagerUtils.GetHeader("Server DNS")); + + VisualElement row = new VisualElement(); + row.AddToClassList("row__dns"); + row.AddToClassList("focusable"); + + row.Add(new Label(serverDns)); + row.Add(EdgegapServerDataManagerUtils.GetCopyButton("Copy", serverDns)); + + container.Add(row); + + return container; + } + + static VisualElement GetPortsSection() + { + List serverPorts = _serverData.Ports.Values.ToList(); + + VisualElement container = new VisualElement(); + container.AddToClassList("container"); + + container.Add(EdgegapServerDataManagerUtils.GetHeader("Server Ports")); + container.Add(EdgegapServerDataManagerUtils.GetHeaderRow()); + + VisualElement portList = new VisualElement(); + + if (serverPorts.Count > 0) + { + foreach (PortMapping port in serverPorts) + { + portList.Add(EdgegapServerDataManagerUtils.GetRowFromPortResponse(port)); + } + } + else + { + portList.Add(new Label("No port configured for this app version.")); + } + + container.Add(portList); + + return container; + } + + public static VisualElement GetServerDataVisualTree() + { + VisualElement serverDataTree = new VisualElement(); + serverDataTree.styleSheets.Add(_serverDataStylesheet); + + bool hasServerData = _serverData != null; + bool isReady = hasServerData && _serverData.GetServerStatus().IsOneOf(ServerStatus.Ready, ServerStatus.Error); + + if (hasServerData) + { + serverDataTree.Add(GetStatusSection()); + + if (isReady) + { + serverDataTree.Add(GetDnsSection()); + serverDataTree.Add(GetPortsSection()); + } + else + { + serverDataTree.Add(EdgegapServerDataManagerUtils.GetInfoText("Additionnal information will be displayed when the server is ready.")); + } + } + else + { + serverDataTree.Add(EdgegapServerDataManagerUtils.GetInfoText("Server data will be displayed here when a server is running.")); + } + + return serverDataTree; + } + + static void RefreshServerDataContainers() + { + foreach (VisualElement serverDataContainer in _serverDataContainers) + { + serverDataContainer.Clear(); + serverDataContainer.Add(GetServerDataVisualTree()); // Cannot reuse a same instance of VisualElement + } + } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerDataManager.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerDataManager.cs.meta new file mode 100755 index 000000000..65da2ab42 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerDataManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 39f5f27c13279a34eb116630a00e41c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapToolScript.cs b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapToolScript.cs new file mode 100755 index 000000000..a949f7cf9 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapToolScript.cs @@ -0,0 +1,15 @@ +using UnityEngine; +using Edgegap; +using IO.Swagger.Model; + +namespace Edgegap +{ + /// + /// This script acts as an interface to display and use the necessary variables from the Edgegap tool. + /// The server info can be accessed from the tool window, as well as through the public script property. + /// + public class EdgegapToolScript : MonoBehaviour + { + public Status ServerStatus => EdgegapServerDataManager.GetServerStatus(); + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapToolScript.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapToolScript.cs.meta new file mode 100755 index 000000000..d584893d2 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapToolScript.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5963202433da25448a22def99f5a598b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindow.cs b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindow.cs new file mode 100755 index 000000000..450de2d26 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindow.cs @@ -0,0 +1,698 @@ +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; +using UnityEditor.UIElements; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Collections.Generic; +using Newtonsoft.Json; +using System.Net; +using System.Text; +using System; +using System.Threading.Tasks; +using IO.Swagger.Model; +using UnityEditor.Build.Reporting; +using Application = UnityEngine.Application; + +namespace Edgegap +{ + public class EdgegapWindow : EditorWindow + { + static readonly HttpClient _httpClient = new HttpClient(); + + const string EditorDataSerializationName = "EdgegapSerializationData"; + const int ServerStatusCronjobIntervalMs = 10000; // Interval at which the server status is updated + + // MIRROR CHANGE: specify stylesheet paths in one place + // TODO DON'T HARDCODE + public const string StylesheetPath = "Assets/Mirror/Hosting/Edgegap/Editor"; + // END MIRROR CHANGE + + readonly System.Timers.Timer _updateServerStatusCronjob = new System.Timers.Timer(ServerStatusCronjobIntervalMs); + + [SerializeField] string _userExternalIp; + [SerializeField] string _apiKey; + [SerializeField] ApiEnvironment _apiEnvironment; + [SerializeField] string _appName; + [SerializeField] string _appVersionName; + [SerializeField] string _deploymentRequestId; + + [SerializeField] string _containerRegistry; + [SerializeField] string _containerImageRepo; + [SerializeField] string _containerImageTag; + [SerializeField] bool _autoIncrementTag = true; + + + VisualTreeAsset _visualTree; + bool _shouldUpdateServerStatus = false; + + // Interactable elements + EnumField _apiEnvironmentSelect; + TextField _apiKeyInput; + TextField _appNameInput; + TextField _appVersionNameInput; + TextField _containerRegistryInput; + TextField _containerImageRepoInput; + TextField _containerImageTagInput; + Toggle _autoIncrementTagInput; + Button _connectionButton; + Button _serverActionButton; + Button _documentationBtn; + Button _buildAndPushServerBtn; + + // Readonly elements + Label _connectionStatusLabel; + VisualElement _serverDataContainer; + + [MenuItem("Edgegap/Edgegap Hosting")] // MIRROR CHANGE + public static void ShowEdgegapToolWindow() + { + EdgegapWindow window = GetWindow(); + window.titleContent = new GUIContent("Edgegap Hosting"); // MIRROR CHANGE + } + + protected void OnEnable() + { + // Set root VisualElement and style + // BEGIN MIRROR CHANGE + _visualTree = AssetDatabase.LoadAssetAtPath($"{StylesheetPath}/EdgegapWindow.uxml"); + StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath($"{StylesheetPath}/EdgegapWindow.uss"); + // END MIRROR CHANGE + rootVisualElement.styleSheets.Add(styleSheet); + + LoadToolData(); + + if (string.IsNullOrWhiteSpace(_userExternalIp)) + { + _userExternalIp = GetExternalIpAddress(); + } + } + + protected void Update() + { + if (_shouldUpdateServerStatus) + { + _shouldUpdateServerStatus = false; + UpdateServerStatus(); + } + } + + public void CreateGUI() + { + rootVisualElement.Clear(); + _visualTree.CloneTree(rootVisualElement); + + InitUIElements(); + SyncFormWithObject(); + + bool hasActiveDeployment = !string.IsNullOrEmpty(_deploymentRequestId); + + if (hasActiveDeployment) + { + RestoreActiveDeployment(); + } + else + { + DisconnectCallback(); + } + } + + protected void OnDestroy() + { + bool deploymentActive = !string.IsNullOrEmpty(_deploymentRequestId); + + if (deploymentActive) + { + EditorUtility.DisplayDialog( + "Warning", + $"You have an active deployment ({_deploymentRequestId}) that won't be stopped automatically.", + "Ok" + ); + } + } + + protected void OnDisable() + { + SyncObjectWithForm(); + SaveToolData(); + EdgegapServerDataManager.DeregisterServerDataContainer(_serverDataContainer); + } + + /// + /// Binds the form inputs to the associated variables and initializes the inputs as required. + /// Requires the VisualElements to be loaded before this call. Otherwise, the elements cannot be found. + /// + void InitUIElements() + { + _apiEnvironmentSelect = rootVisualElement.Q("environmentSelect"); + _apiKeyInput = rootVisualElement.Q("apiKey"); + _appNameInput = rootVisualElement.Q("appName"); + _appVersionNameInput = rootVisualElement.Q("appVersionName"); + + _containerRegistryInput = rootVisualElement.Q("containerRegistry"); + _containerImageRepoInput = rootVisualElement.Q("containerImageRepo"); + _containerImageTagInput = rootVisualElement.Q("tag"); + _autoIncrementTagInput = rootVisualElement.Q("autoIncrementTag"); + + _connectionButton = rootVisualElement.Q