EdgegapWindow.cs 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986
  1. // MIRROR CHANGE: disable this completely. otherwise InitUIElements can still throw NRE.
  2. /*
  3. using UnityEditor;
  4. using UnityEngine;
  5. using UnityEngine.UIElements;
  6. using UnityEditor.UIElements;
  7. using System.Net.Http;
  8. using System.Net.Http.Headers;
  9. using System.Collections.Generic;
  10. using Newtonsoft.Json;
  11. using System.Net;
  12. using System.Text;
  13. using System;
  14. using System.IO;
  15. using System.Linq;
  16. using System.Runtime.InteropServices;
  17. using System.Text.RegularExpressions;
  18. using System.Threading.Tasks;
  19. using IO.Swagger.Model;
  20. using UnityEditor.Build.Reporting;
  21. using Application = UnityEngine.Application;
  22. namespace Edgegap
  23. {
  24. public class EdgegapWindow : EditorWindow
  25. {
  26. // MIRROR CHANGE: create HTTPClient in-place to avoid InvalidOperationExceptions when reusing
  27. // static readonly HttpClient _httpClient = new HttpClient();
  28. // END MIRROR CHANGE
  29. const string EditorDataSerializationName = "EdgegapSerializationData";
  30. const int ServerStatusCronjobIntervalMs = 10000; // Interval at which the server status is updated
  31. // MIRROR CHANGE
  32. // get the path of this .cs file so we don't need to hardcode paths to
  33. // the .uxml and .uss files:
  34. // https://forum.unity.com/threads/too-many-hard-coded-paths-in-the-templates-and-documentation.728138/
  35. // this way users can move this folder without breaking UIToolkit paths.
  36. internal string StylesheetPath =>
  37. Path.GetDirectoryName(AssetDatabase.GetAssetPath(MonoScript.FromScriptableObject(this)));
  38. // END MIRROR CHANGE
  39. readonly System.Timers.Timer _updateServerStatusCronjob = new System.Timers.Timer(ServerStatusCronjobIntervalMs);
  40. [SerializeField] string _userExternalIp;
  41. [SerializeField] string _apiKey;
  42. [SerializeField] ApiEnvironment _apiEnvironment;
  43. [SerializeField] string _appName;
  44. [SerializeField] string _appVersionName;
  45. [SerializeField] string _deploymentRequestId;
  46. [SerializeField] string _containerRegistry;
  47. [SerializeField] string _containerImageRepo;
  48. [SerializeField] string _containerImageTag;
  49. [SerializeField] bool _autoIncrementTag = true;
  50. VisualTreeAsset _visualTree;
  51. bool _shouldUpdateServerStatus = false;
  52. // Interactable elements
  53. EnumField _apiEnvironmentSelect;
  54. TextField _apiKeyInput;
  55. TextField _appNameInput;
  56. TextField _appVersionNameInput;
  57. TextField _containerRegistryInput;
  58. TextField _containerImageRepoInput;
  59. TextField _containerImageTagInput;
  60. Toggle _autoIncrementTagInput;
  61. Button _connectionButton;
  62. Button _serverActionButton;
  63. Button _documentationBtn;
  64. Button _buildAndPushServerBtn;
  65. // Readonly elements
  66. Label _connectionStatusLabel;
  67. VisualElement _serverDataContainer;
  68. // server data manager
  69. StyleSheet _serverDataStylesheet;
  70. List<VisualElement> _serverDataContainers = new List<VisualElement>();
  71. [Obsolete("See EdgegapWindowV2.ShowEdgegapToolWindow()")]
  72. // [MenuItem("Edgegap/Server Management")]
  73. public static void ShowEdgegapToolWindow()
  74. {
  75. EdgegapWindow window = GetWindow<EdgegapWindow>();
  76. window.titleContent = new GUIContent("Edgegap Hosting"); // MIRROR CHANGE
  77. }
  78. protected void OnEnable()
  79. {
  80. // Set root VisualElement and style
  81. // BEGIN MIRROR CHANGE
  82. _visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>($"{StylesheetPath}/EdgegapWindow.uxml");
  83. StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>($"{StylesheetPath}/EdgegapWindow.uss");
  84. _serverDataStylesheet = AssetDatabase.LoadAssetAtPath<StyleSheet>($"{StylesheetPath}/EdgegapServerData.uss");
  85. // END MIRROR CHANGE
  86. rootVisualElement.styleSheets.Add(styleSheet);
  87. LoadToolData();
  88. if (string.IsNullOrWhiteSpace(_userExternalIp))
  89. {
  90. _userExternalIp = GetExternalIpAddress();
  91. }
  92. }
  93. protected void Update()
  94. {
  95. if (_shouldUpdateServerStatus)
  96. {
  97. _shouldUpdateServerStatus = false;
  98. UpdateServerStatus();
  99. }
  100. }
  101. public void CreateGUI()
  102. {
  103. rootVisualElement.Clear();
  104. _visualTree.CloneTree(rootVisualElement);
  105. InitUIElements();
  106. SyncFormWithObject();
  107. bool hasActiveDeployment = !string.IsNullOrEmpty(_deploymentRequestId);
  108. if (hasActiveDeployment)
  109. {
  110. RestoreActiveDeployment();
  111. }
  112. else
  113. {
  114. DisconnectCallback();
  115. }
  116. }
  117. protected void OnDestroy()
  118. {
  119. bool deploymentActive = !string.IsNullOrEmpty(_deploymentRequestId);
  120. if (deploymentActive)
  121. {
  122. EditorUtility.DisplayDialog(
  123. "Warning",
  124. $"You have an active deployment ({_deploymentRequestId}) that won't be stopped automatically.",
  125. "Ok"
  126. );
  127. }
  128. }
  129. protected void OnDisable()
  130. {
  131. SyncObjectWithForm();
  132. SaveToolData();
  133. DeregisterServerDataContainer(_serverDataContainer);
  134. }
  135. /// <summary>
  136. /// Binds the form inputs to the associated variables and initializes the inputs as required.
  137. /// Requires the VisualElements to be loaded before this call. Otherwise, the elements cannot be found.
  138. /// </summary>
  139. void InitUIElements()
  140. {
  141. _apiEnvironmentSelect = rootVisualElement.Q<EnumField>("environmentSelect");
  142. _apiKeyInput = rootVisualElement.Q<TextField>("apiKey");
  143. _appNameInput = rootVisualElement.Q<TextField>("appName");
  144. _appVersionNameInput = rootVisualElement.Q<TextField>("appVersionName");
  145. _containerRegistryInput = rootVisualElement.Q<TextField>("containerRegistry");
  146. _containerImageRepoInput = rootVisualElement.Q<TextField>("containerImageRepo");
  147. _containerImageTagInput = rootVisualElement.Q<TextField>("tag");
  148. _autoIncrementTagInput = rootVisualElement.Q<Toggle>("autoIncrementTag");
  149. _connectionButton = rootVisualElement.Q<Button>("connectionBtn");
  150. _serverActionButton = rootVisualElement.Q<Button>("serverActionBtn");
  151. _documentationBtn = rootVisualElement.Q<Button>("documentationBtn");
  152. _buildAndPushServerBtn = rootVisualElement.Q<Button>("buildAndPushBtn");
  153. _buildAndPushServerBtn.clickable.clicked += BuildAndPushServer;
  154. _connectionStatusLabel = rootVisualElement.Q<Label>("connectionStatusLabel");
  155. _serverDataContainer = rootVisualElement.Q<VisualElement>("serverDataContainer");
  156. // Load initial server data UI element and register for updates.
  157. VisualElement serverDataElement = GetServerDataVisualTree();
  158. RegisterServerDataContainer(serverDataElement);
  159. _serverDataContainer.Clear();
  160. _serverDataContainer.Add(serverDataElement);
  161. _documentationBtn.clickable.clicked += OpenDocumentationCallback;
  162. // Init the ApiEnvironment dropdown
  163. _apiEnvironmentSelect.Init(ApiEnvironment.Console);
  164. }
  165. /// <summary>
  166. /// With a call to an external resource, determines the current user's public IP address.
  167. /// </summary>
  168. /// <returns>External IP address</returns>
  169. string GetExternalIpAddress()
  170. {
  171. string externalIpString = new WebClient()
  172. .DownloadString("http://icanhazip.com")
  173. .Replace("\\r\\n", "")
  174. .Replace("\\n", "")
  175. .Trim();
  176. IPAddress externalIp = IPAddress.Parse(externalIpString);
  177. return externalIp.ToString();
  178. }
  179. void OpenDocumentationCallback()
  180. {
  181. // MIRROR CHANGE
  182. // ApiEnvironment selectedApiEnvironment = (ApiEnvironment)_apiEnvironmentSelect.value;
  183. // string documentationUrl = selectedApiEnvironment.GetDocumentationUrl();
  184. //
  185. // if (!string.IsNullOrEmpty(documentationUrl))
  186. // {
  187. // UnityEngine.Application.OpenURL(documentationUrl);
  188. // }
  189. // else
  190. // {
  191. // string apiEnvName = Enum.GetName(typeof(ApiEnvironment), selectedApiEnvironment);
  192. // Debug.LogWarning($"Could not open documentation for api environment {apiEnvName}: No documentation URL.");
  193. // }
  194. // link to the easiest documentation
  195. Application.OpenURL("https://mirror-networking.gitbook.io/docs/hosting/edgegap-hosting-plugin-guide");
  196. // END MIRROR CHANGE
  197. }
  198. void ConnectCallback()
  199. {
  200. ApiEnvironment selectedApiEnvironment = (ApiEnvironment)_apiEnvironmentSelect.value;
  201. string selectedAppName = _appNameInput.value;
  202. string selectedVersionName = _appVersionNameInput.value;
  203. string selectedApiKey = _apiKeyInput.value;
  204. bool validAppName = !string.IsNullOrEmpty(selectedAppName) && !string.IsNullOrWhiteSpace(selectedAppName);
  205. bool validVersionName = !string.IsNullOrEmpty(selectedVersionName) && !string.IsNullOrWhiteSpace(selectedVersionName);
  206. bool validApiKey = selectedApiKey.StartsWith("token ");
  207. if (validAppName && validVersionName && validApiKey)
  208. {
  209. string apiKeyValue = selectedApiKey.Substring(6);
  210. Connect(selectedApiEnvironment, selectedAppName, selectedVersionName, apiKeyValue);
  211. }
  212. else
  213. {
  214. EditorUtility.DisplayDialog(
  215. "Could not connect - Invalid data",
  216. "The data provided is invalid. " +
  217. "Make sure every field is filled, and that you provide your complete Edgegap API token " +
  218. "(including the \"token\" part).",
  219. "Ok"
  220. );
  221. }
  222. }
  223. // MIRROR CHANGE: create HTTPClient in-place to avoid InvalidOperationExceptions when reusing
  224. HttpClient CreateHttpClient()
  225. {
  226. HttpClient httpClient = new HttpClient();
  227. httpClient.BaseAddress = new Uri(_apiEnvironment.GetApiUrl());
  228. httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
  229. string token = _apiKeyInput.value.Substring(6);
  230. httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", token);
  231. return httpClient;
  232. }
  233. // END MIRROR CHANGE
  234. async void Connect(
  235. ApiEnvironment selectedApiEnvironment,
  236. string selectedAppName,
  237. string selectedAppVersionName,
  238. string selectedApiTokenValue
  239. )
  240. {
  241. SetToolUIState(ToolState.Connecting);
  242. // Make HTTP request
  243. HttpClient _httpClient = CreateHttpClient(); // MIRROR CHANGE: create HTTPClient in-place to avoid InvalidOperationExceptions when reusing
  244. string path = $"/v1/app/{selectedAppName}/version/{selectedAppVersionName}"; // MIRROR CHANGE: use selectedAppName and selectedAppVersionName instead of _appName & _appVersionName
  245. HttpResponseMessage response = await _httpClient.GetAsync(path);
  246. if (response.IsSuccessStatusCode)
  247. {
  248. SyncObjectWithForm();
  249. SetToolUIState(ToolState.Connected);
  250. }
  251. else
  252. {
  253. int status = (int)response.StatusCode;
  254. string title;
  255. string message;
  256. if (status == 401)
  257. {
  258. string apiEnvName = Enum.GetName(typeof(ApiEnvironment), selectedApiEnvironment);
  259. title = "Invalid credentials";
  260. message = $"Could not find an Edgegap account with this API key for the {apiEnvName} environment.";
  261. }
  262. else if (status == 404)
  263. {
  264. title = "App not found";
  265. message = $"Could not find app {selectedAppName} with version {selectedAppVersionName}.";
  266. }
  267. else
  268. {
  269. title = "Oops";
  270. message = $"There was an error while connecting you to the Edgegap API. Please try again later.";
  271. }
  272. EditorUtility.DisplayDialog(title, message, "Ok");
  273. SetToolUIState(ToolState.Disconnected);
  274. }
  275. }
  276. void DisconnectCallback()
  277. {
  278. if (string.IsNullOrEmpty(_deploymentRequestId))
  279. {
  280. SetToolUIState(ToolState.Disconnected);
  281. }
  282. else
  283. {
  284. EditorUtility.DisplayDialog("Cannot disconnect", "Make sure no server is running in the Edgegap tool before disconnecting", "Ok");
  285. }
  286. }
  287. float ProgressCounter = 0;
  288. // MIRROR CHANGE: added title parameter for more detailed progress while waiting
  289. void ShowBuildWorkInProgress(string title, string status)
  290. {
  291. EditorUtility.DisplayProgressBar(title, status, ProgressCounter++ / 50);
  292. }
  293. // END MIRROR CHANGE
  294. async void BuildAndPushServer()
  295. {
  296. SetToolUIState(ToolState.Building);
  297. SyncObjectWithForm();
  298. ProgressCounter = 0;
  299. Action<string> onError = (msg) =>
  300. {
  301. EditorUtility.DisplayDialog("Error", msg, "Ok");
  302. SetToolUIState(ToolState.Connected);
  303. };
  304. try
  305. {
  306. // check for installation and setup docker file
  307. if (!await EdgegapBuildUtils.DockerSetupAndInstallationCheck())
  308. {
  309. onError("Docker installation not found. Docker can be downloaded from:\n\nhttps://www.docker.com/");
  310. return;
  311. }
  312. // MIRROR CHANGE
  313. // make sure Linux build target is installed before attemping to build.
  314. // if it's not installed, tell the user about it.
  315. if (!BuildPipeline.IsBuildTargetSupported(BuildTargetGroup.Standalone, BuildTarget.StandaloneLinux64))
  316. {
  317. onError($"Linux Build Support is missing.\n\nPlease open Unity Hub -> Installs -> Unity {Application.unityVersion} -> Add Modules -> Linux Build Support (IL2CPP & Mono & Dedicated Server) -> Install\n\nAfterwards restart Unity!");
  318. return;
  319. }
  320. // END MIRROR CHANGE
  321. // create server build
  322. BuildReport buildResult = EdgegapBuildUtils.BuildServer();
  323. if (buildResult.summary.result != BuildResult.Succeeded)
  324. {
  325. onError("Edgegap build failed, please check the Unity console logs.");
  326. return;
  327. }
  328. string registry = _containerRegistry;
  329. string imageName = _containerImageRepo;
  330. string tag = _containerImageTag;
  331. // MIRROR CHANGE ///////////////////////////////////////////////
  332. // registry, repository and tag can not contain whitespaces.
  333. // otherwise the docker command will throw an error:
  334. // "ERROR: "docker buildx build" requires exactly 1 argument."
  335. // catch this early and notify the user immediately.
  336. if (registry.Contains(" "))
  337. {
  338. onError($"Container Registry is not allowed to contain whitespace: '{registry}'");
  339. return;
  340. }
  341. if (imageName.Contains(" "))
  342. {
  343. onError($"Image Repository is not allowed to contain whitespace: '{imageName}'");
  344. return;
  345. }
  346. if (tag.Contains(" "))
  347. {
  348. onError($"Tag is not allowed to contain whitespace: '{tag}'");
  349. return;
  350. }
  351. // END MIRROR CHANGE ///////////////////////////////////////////
  352. // increment tag for quicker iteration
  353. if (_autoIncrementTag)
  354. {
  355. tag = EdgegapBuildUtils.IncrementTag(tag);
  356. }
  357. // create docker image
  358. await EdgegapBuildUtils.RunCommand_DockerBuild(registry, imageName, tag, status => ShowBuildWorkInProgress("Building Docker Image", status));
  359. SetToolUIState(ToolState.Pushing);
  360. // push docker image
  361. (bool result, string error) = await EdgegapBuildUtils.RunCommand_DockerPush(registry, imageName, tag, status => ShowBuildWorkInProgress("Uploading Docker Image (this may take a while)", status));
  362. if (!result)
  363. {
  364. // catch common issues with detailed solutions
  365. if (error.Contains("Cannot connect to the Docker daemon"))
  366. {
  367. onError($"{error}\nTo solve this, you can install and run Docker Desktop from:\n\nhttps://www.docker.com/products/docker-desktop");
  368. return;
  369. }
  370. if (error.Contains("unauthorized to access repository"))
  371. {
  372. onError($"Docker authorization failed:\n\n{error}\nTo solve this, you can open a terminal and enter 'docker login {registry}', then enter your credentials.");
  373. return;
  374. }
  375. // project not found?
  376. if (Regex.IsMatch(error, @".*project .* not found.*", RegexOptions.IgnoreCase))
  377. {
  378. onError($"{error}\nTo solve this, make sure that Image Repository is 'project/game' where 'project' is from the Container Registry page on the Edgegap website.");
  379. return;
  380. }
  381. // otherwise show generic error message
  382. onError($"Unable to push docker image to registry. Please make sure you're logged in to {registry} and check the following error:\n\n{error}");
  383. return;
  384. }
  385. // update edgegap server settings for new tag
  386. ShowBuildWorkInProgress("Build and Push", "Updating server info on Edgegap");
  387. await UpdateAppTagOnEdgegap(tag);
  388. // cleanup
  389. _containerImageTag = tag;
  390. SyncFormWithObject();
  391. SetToolUIState(ToolState.Connected);
  392. Debug.Log("Server built and pushed successfully");
  393. }
  394. catch (Exception ex)
  395. {
  396. Debug.LogError(ex);
  397. onError($"Edgegap build and push failed with Error: {ex}");
  398. }
  399. finally
  400. {
  401. // MIRROR CHANGE: always clear otherwise it gets stuck there forever!
  402. EditorUtility.ClearProgressBar();
  403. }
  404. }
  405. async Task UpdateAppTagOnEdgegap(string newTag)
  406. {
  407. string path = $"/v1/app/{_appName}/version/{_appVersionName}";
  408. // Setup post data
  409. AppVersionUpdatePatchData updatePatchData = new AppVersionUpdatePatchData { DockerImage = _containerImageRepo, DockerRegistry = _containerRegistry, DockerTag = newTag };
  410. string json = JsonConvert.SerializeObject(updatePatchData);
  411. StringContent patchData = new StringContent(json, Encoding.UTF8, "application/json");
  412. // Make HTTP request
  413. HttpClient _httpClient = CreateHttpClient(); // MIRROR CHANGE: create HTTPClient in-place to avoid InvalidOperationExceptions when reusing
  414. HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), path);
  415. request.Content = patchData;
  416. HttpResponseMessage response = await _httpClient.SendAsync(request);
  417. string content = await response.Content.ReadAsStringAsync();
  418. if (!response.IsSuccessStatusCode)
  419. {
  420. throw new Exception($"Could not update Edgegap server tag. Got {(int)response.StatusCode} with response:\n{content}");
  421. }
  422. }
  423. async void StartServerCallback()
  424. {
  425. SetToolUIState(ToolState.ProcessingDeployment); // Prevents being called multiple times.
  426. const string path = "/v1/deploy";
  427. // Setup post data
  428. DeployPostData deployPostData = new DeployPostData(_appName, _appVersionName, new List<string> { _userExternalIp });
  429. string json = JsonConvert.SerializeObject(deployPostData);
  430. StringContent postData = new StringContent(json, Encoding.UTF8, "application/json");
  431. // Make HTTP request
  432. HttpClient _httpClient = CreateHttpClient(); // MIRROR CHANGE: create HTTPClient in-place to avoid InvalidOperationExceptions when reusing
  433. HttpResponseMessage response = await _httpClient.PostAsync(path, postData);
  434. string content = await response.Content.ReadAsStringAsync();
  435. if (response.IsSuccessStatusCode)
  436. {
  437. // Parse response
  438. Deployment parsedResponse = JsonConvert.DeserializeObject<Deployment>(content);
  439. _deploymentRequestId = parsedResponse.RequestId;
  440. UpdateServerStatus();
  441. StartServerStatusCronjob();
  442. }
  443. else
  444. {
  445. Debug.LogError($"Could not start Edgegap server. Got {(int)response.StatusCode} with response:\n{content}");
  446. SetToolUIState(ToolState.Connected);
  447. }
  448. }
  449. async void StopServerCallback()
  450. {
  451. string path = $"/v1/stop/{_deploymentRequestId}";
  452. // Make HTTP request
  453. HttpClient _httpClient = CreateHttpClient(); // MIRROR CHANGE: create HTTPClient in-place to avoid InvalidOperationExceptions when reusing
  454. HttpResponseMessage response = await _httpClient.DeleteAsync(path);
  455. if (response.IsSuccessStatusCode)
  456. {
  457. UpdateServerStatus();
  458. SetToolUIState(ToolState.ProcessingDeployment);
  459. }
  460. else
  461. {
  462. // Parse response
  463. string content = await response.Content.ReadAsStringAsync();
  464. Debug.LogError($"Could not stop Edgegap server. Got {(int)response.StatusCode} with response:\n{content}");
  465. }
  466. }
  467. void StartServerStatusCronjob()
  468. {
  469. _updateServerStatusCronjob.Elapsed += (sourceObject, elaspedEvent) => _shouldUpdateServerStatus = true;
  470. _updateServerStatusCronjob.AutoReset = true;
  471. _updateServerStatusCronjob.Start();
  472. }
  473. void StopServerStatusCronjob() => _updateServerStatusCronjob.Stop();
  474. async void UpdateServerStatus()
  475. {
  476. Status serverStatusResponse = await FetchServerStatus();
  477. ToolState toolState;
  478. ServerStatus serverStatus = serverStatusResponse.GetServerStatus();
  479. if (serverStatus == ServerStatus.Terminated)
  480. {
  481. SetServerData(null, _apiEnvironment);
  482. if (_updateServerStatusCronjob.Enabled)
  483. {
  484. StopServerStatusCronjob();
  485. }
  486. _deploymentRequestId = null;
  487. toolState = ToolState.Connected;
  488. }
  489. else
  490. {
  491. SetServerData(serverStatusResponse, _apiEnvironment);
  492. if (serverStatus == ServerStatus.Ready || serverStatus == ServerStatus.Error)
  493. {
  494. toolState = ToolState.DeploymentRunning;
  495. }
  496. else
  497. {
  498. toolState = ToolState.ProcessingDeployment;
  499. }
  500. }
  501. SetToolUIState(toolState);
  502. }
  503. async Task<Status> FetchServerStatus()
  504. {
  505. string path = $"/v1/status/{_deploymentRequestId}";
  506. // Make HTTP request
  507. HttpClient _httpClient = CreateHttpClient(); // MIRROR CHANGE: create HTTPClient in-place to avoid InvalidOperationExceptions when reusing
  508. HttpResponseMessage response = await _httpClient.GetAsync(path);
  509. // Parse response
  510. string content = await response.Content.ReadAsStringAsync();
  511. Status parsedData;
  512. if (response.IsSuccessStatusCode)
  513. {
  514. parsedData = JsonConvert.DeserializeObject<Status>(content);
  515. }
  516. else
  517. {
  518. if ((int)response.StatusCode == 400)
  519. {
  520. Debug.LogError("The deployment that was active in the tool is now unreachable. Considering it Terminated.");
  521. parsedData = new Status() { CurrentStatus = ServerStatus.Terminated.GetLabelText() };
  522. }
  523. else
  524. {
  525. Debug.LogError(
  526. $"Could not fetch status of Edgegap deployment {_deploymentRequestId}. " +
  527. $"Got {(int)response.StatusCode} with response:\n{content}"
  528. );
  529. parsedData = new Status() { CurrentStatus = ServerStatus.NA.GetLabelText() };
  530. }
  531. }
  532. return parsedData;
  533. }
  534. void RestoreActiveDeployment()
  535. {
  536. ConnectCallback();
  537. _shouldUpdateServerStatus = true;
  538. StartServerStatusCronjob();
  539. }
  540. void SyncObjectWithForm()
  541. {
  542. if (_apiKeyInput == null) return; // MIRROR CHANGE: fix NRE when this is called before UI elements were assgned
  543. _apiKey = _apiKeyInput.value;
  544. _apiEnvironment = (ApiEnvironment)_apiEnvironmentSelect.value;
  545. _appName = _appNameInput.value;
  546. _appVersionName = _appVersionNameInput.value;
  547. // MIRROR CHANGE ///////////////////////////////////////////////////
  548. // registry, repository and tag can not contain whitespaces.
  549. // otherwise it'll throw an error:
  550. // "ERROR: "docker buildx build" requires exactly 1 argument."
  551. // trim whitespace in case users accidentally added some.
  552. _containerRegistry = _containerRegistryInput.value.Trim();
  553. _containerImageTag = _containerImageTagInput.value.Trim();
  554. _containerImageRepo = _containerImageRepoInput.value.Trim();
  555. // END MIRROR CHANGE ///////////////////////////////////////////////
  556. _autoIncrementTag = _autoIncrementTagInput.value;
  557. }
  558. void SyncFormWithObject()
  559. {
  560. _apiKeyInput.value = _apiKey;
  561. _apiEnvironmentSelect.value = _apiEnvironment;
  562. _appNameInput.value = _appName;
  563. _appVersionNameInput.value = _appVersionName;
  564. _containerRegistryInput.value = _containerRegistry;
  565. _containerImageTagInput.value = _containerImageTag;
  566. _containerImageRepoInput.value = _containerImageRepo;
  567. _autoIncrementTagInput.value = _autoIncrementTag;
  568. }
  569. void SetToolUIState(ToolState toolState)
  570. {
  571. SetConnectionInfoUI(toolState);
  572. SetConnectionButtonUI(toolState);
  573. SetServerActionUI(toolState);
  574. SetDockerRepoInfoUI(toolState);
  575. }
  576. void SetDockerRepoInfoUI(ToolState toolState)
  577. {
  578. bool connected = toolState.CanStartDeployment();
  579. _containerRegistryInput.SetEnabled(connected);
  580. _autoIncrementTagInput.SetEnabled(connected);
  581. _containerImageRepoInput.SetEnabled(connected);
  582. _containerImageTagInput.SetEnabled(connected);
  583. }
  584. void SetConnectionInfoUI(ToolState toolState)
  585. {
  586. bool canEditConnectionInfo = toolState.CanEditConnectionInfo();
  587. _apiKeyInput.SetEnabled(canEditConnectionInfo);
  588. _apiEnvironmentSelect.SetEnabled(canEditConnectionInfo);
  589. _appNameInput.SetEnabled(canEditConnectionInfo);
  590. _appVersionNameInput.SetEnabled(canEditConnectionInfo);
  591. }
  592. void SetConnectionButtonUI(ToolState toolState)
  593. {
  594. bool canConnect = toolState.CanConnect();
  595. bool canDisconnect = toolState.CanDisconnect();
  596. _connectionButton.SetEnabled(canConnect || canDisconnect);
  597. // A bit dirty, but ensures the callback is not bound multiple times on the button.
  598. _connectionButton.clickable.clicked -= ConnectCallback;
  599. _connectionButton.clickable.clicked -= DisconnectCallback;
  600. if (canConnect || toolState == ToolState.Connecting)
  601. {
  602. _connectionButton.text = "Connect";
  603. _connectionStatusLabel.text = "Awaiting connection";
  604. _connectionStatusLabel.RemoveFromClassList("text--success");
  605. _connectionButton.clickable.clicked += ConnectCallback;
  606. }
  607. else
  608. {
  609. _connectionButton.text = "Disconnect";
  610. _connectionStatusLabel.text = "Connected";
  611. _connectionStatusLabel.AddToClassList("text--success");
  612. _connectionButton.clickable.clicked += DisconnectCallback;
  613. }
  614. }
  615. void SetServerActionUI(ToolState toolState)
  616. {
  617. bool canStartDeployment = toolState.CanStartDeployment();
  618. bool canStopDeployment = toolState.CanStopDeployment();
  619. // A bit dirty, but ensures the callback is not bound multiple times on the button.
  620. _serverActionButton.clickable.clicked -= StartServerCallback;
  621. _serverActionButton.clickable.clicked -= StopServerCallback;
  622. _serverActionButton.SetEnabled(canStartDeployment || canStopDeployment);
  623. _buildAndPushServerBtn.SetEnabled(canStartDeployment);
  624. if (canStopDeployment)
  625. {
  626. _serverActionButton.text = "Stop Server";
  627. _serverActionButton.clickable.clicked += StopServerCallback;
  628. }
  629. else
  630. {
  631. _serverActionButton.text = "Start Server";
  632. _serverActionButton.clickable.clicked += StartServerCallback;
  633. }
  634. }
  635. // server data manager /////////////////////////////////////////////////
  636. public void RegisterServerDataContainer(VisualElement serverDataContainer)
  637. {
  638. _serverDataContainers.Add(serverDataContainer);
  639. }
  640. public void DeregisterServerDataContainer(VisualElement serverDataContainer)
  641. {
  642. _serverDataContainers.Remove(serverDataContainer);
  643. }
  644. public void SetServerData(Status serverData, ApiEnvironment apiEnvironment)
  645. {
  646. EdgegapServerDataManager._serverData = serverData;
  647. RefreshServerDataContainers();
  648. }
  649. public Label GetHeader(string text)
  650. {
  651. Label header = new Label(text);
  652. header.AddToClassList("label__header");
  653. return header;
  654. }
  655. public VisualElement GetHeaderRow()
  656. {
  657. VisualElement row = new VisualElement();
  658. row.AddToClassList("row__port-table");
  659. row.AddToClassList("label__header");
  660. row.Add(new Label("Name"));
  661. row.Add(new Label("External"));
  662. row.Add(new Label("Internal"));
  663. row.Add(new Label("Protocol"));
  664. row.Add(new Label("Link"));
  665. return row;
  666. }
  667. public VisualElement GetRowFromPortResponse(PortMapping port)
  668. {
  669. VisualElement row = new VisualElement();
  670. row.AddToClassList("row__port-table");
  671. row.AddToClassList("focusable");
  672. row.Add(new Label(port.Name));
  673. row.Add(new Label(port.External.ToString()));
  674. row.Add(new Label(port.Internal.ToString()));
  675. row.Add(new Label(port.Protocol));
  676. row.Add(GetCopyButton("Copy", port.Link));
  677. return row;
  678. }
  679. public Button GetCopyButton(string btnText, string copiedText)
  680. {
  681. Button copyBtn = new Button();
  682. copyBtn.text = btnText;
  683. copyBtn.clickable.clicked += () => GUIUtility.systemCopyBuffer = copiedText;
  684. return copyBtn;
  685. }
  686. public Button GetLinkButton(string btnText, string targetUrl)
  687. {
  688. Button copyBtn = new Button();
  689. copyBtn.text = btnText;
  690. copyBtn.clickable.clicked += () => UnityEngine.Application.OpenURL(targetUrl);
  691. return copyBtn;
  692. }
  693. public Label GetInfoText(string innerText)
  694. {
  695. Label infoText = new Label(innerText);
  696. infoText.AddToClassList("label__info-text");
  697. return infoText;
  698. }
  699. VisualElement GetStatusSection()
  700. {
  701. ServerStatus serverStatus = EdgegapServerDataManager._serverData.GetServerStatus();
  702. string dashboardUrl = _apiEnvironment.GetDashboardUrl();
  703. string requestId = EdgegapServerDataManager._serverData.RequestId;
  704. string deploymentDashboardUrl = "";
  705. if (!string.IsNullOrEmpty(requestId) && !string.IsNullOrEmpty(dashboardUrl))
  706. {
  707. deploymentDashboardUrl = $"{dashboardUrl}/arbitrium/deployment/read/{requestId}/";
  708. }
  709. VisualElement container = new VisualElement();
  710. container.AddToClassList("container");
  711. container.Add(GetHeader("Server Status"));
  712. VisualElement row = new VisualElement();
  713. row.AddToClassList("row__status");
  714. // Status pill
  715. Label statusLabel = new Label(serverStatus.GetLabelText());
  716. statusLabel.AddToClassList(serverStatus.GetStatusBgClass());
  717. statusLabel.AddToClassList("label__status");
  718. row.Add(statusLabel);
  719. // Link to dashboard
  720. if (!string.IsNullOrEmpty(deploymentDashboardUrl))
  721. {
  722. row.Add(GetLinkButton("See in the dashboard", deploymentDashboardUrl));
  723. }
  724. else
  725. {
  726. row.Add(new Label("Could not resolve link to this deployment"));
  727. }
  728. container.Add(row);
  729. return container;
  730. }
  731. VisualElement GetDnsSection()
  732. {
  733. string serverDns = EdgegapServerDataManager._serverData.Fqdn;
  734. VisualElement container = new VisualElement();
  735. container.AddToClassList("container");
  736. container.Add(GetHeader("Server DNS"));
  737. VisualElement row = new VisualElement();
  738. row.AddToClassList("row__dns");
  739. row.AddToClassList("focusable");
  740. row.Add(new Label(serverDns));
  741. row.Add(GetCopyButton("Copy", serverDns));
  742. container.Add(row);
  743. return container;
  744. }
  745. VisualElement GetPortsSection()
  746. {
  747. List<PortMapping> serverPorts = EdgegapServerDataManager._serverData.Ports.Values.ToList();
  748. VisualElement container = new VisualElement();
  749. container.AddToClassList("container");
  750. container.Add(GetHeader("Server Ports"));
  751. container.Add(GetHeaderRow());
  752. VisualElement portList = new VisualElement();
  753. if (serverPorts.Count > 0)
  754. {
  755. foreach (PortMapping port in serverPorts)
  756. {
  757. portList.Add(GetRowFromPortResponse(port));
  758. }
  759. }
  760. else
  761. {
  762. portList.Add(new Label("No port configured for this app version."));
  763. }
  764. container.Add(portList);
  765. return container;
  766. }
  767. public VisualElement GetServerDataVisualTree()
  768. {
  769. VisualElement serverDataTree = new VisualElement();
  770. serverDataTree.styleSheets.Add(_serverDataStylesheet);
  771. bool hasServerData = EdgegapServerDataManager._serverData != null;
  772. bool isReady = hasServerData && EdgegapServerDataManager. _serverData.GetServerStatus().IsOneOf(ServerStatus.Ready, ServerStatus.Error);
  773. if (hasServerData)
  774. {
  775. serverDataTree.Add(GetStatusSection());
  776. if (isReady)
  777. {
  778. serverDataTree.Add(GetDnsSection());
  779. serverDataTree.Add(GetPortsSection());
  780. }
  781. else
  782. {
  783. serverDataTree.Add(GetInfoText("Additional information will be displayed when the server is ready."));
  784. }
  785. }
  786. else
  787. {
  788. serverDataTree.Add(GetInfoText("Server data will be displayed here when a server is running."));
  789. }
  790. return serverDataTree;
  791. }
  792. void RefreshServerDataContainers()
  793. {
  794. foreach (VisualElement serverDataContainer in _serverDataContainers)
  795. {
  796. serverDataContainer.Clear();
  797. serverDataContainer.Add(GetServerDataVisualTree()); // Cannot reuse a same instance of VisualElement
  798. }
  799. }
  800. // save & load /////////////////////////////////////////////////////////
  801. /// <summary>
  802. /// Save the tool's serializable data to the EditorPrefs to allow persistence across restarts.
  803. /// Any field with [SerializeField] will be saved.
  804. /// </summary>
  805. void SaveToolData()
  806. {
  807. string data = JsonUtility.ToJson(this, false);
  808. EditorPrefs.SetString(EditorDataSerializationName, data);
  809. }
  810. /// <summary>
  811. /// Load the tool's serializable data from the EditorPrefs to the object, restoring the tool's state.
  812. /// </summary>
  813. void LoadToolData()
  814. {
  815. string data = EditorPrefs.GetString(EditorDataSerializationName, JsonUtility.ToJson(this, false));
  816. JsonUtility.FromJsonOverwrite(data, this);
  817. }
  818. }
  819. }
  820. */