639 lines
19 KiB
C#
639 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Sockets;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using JetBrains.Annotations;
|
|
using Needle.Engine.Utils;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using Debug = UnityEngine.Debug;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace Needle.Engine
|
|
{
|
|
// model: get license
|
|
[Serializable]
|
|
internal class LicenseInformation
|
|
{
|
|
public string user_id;
|
|
public string org_id;
|
|
public string user_name;
|
|
public string user_email;
|
|
public string needle_engine_license;
|
|
public bool needle_engine_license_is_active;
|
|
public string needle_engine_license_status; // active, canceled, incomplete, past_due, unpaid...
|
|
}
|
|
|
|
// model: get/user
|
|
internal class UserInformation
|
|
{
|
|
public string id;
|
|
public string name;
|
|
public string email;
|
|
[CanBeNull] public string profile_picture;
|
|
public string default_org_id;
|
|
public UserOrg[] orgs;
|
|
|
|
internal class UserOrg
|
|
{
|
|
public string id;
|
|
public string name;
|
|
public string[] roles;
|
|
public bool canWrite;
|
|
}
|
|
|
|
public static bool NotEqual(UserInformation currentUserInformation, UserInformation userInformation)
|
|
{
|
|
if (currentUserInformation == null && userInformation != null ||
|
|
currentUserInformation != null && userInformation == null) return true;
|
|
if (currentUserInformation == null && userInformation == null) return false;
|
|
if (currentUserInformation.id != userInformation.id) return true;
|
|
if (currentUserInformation.name != userInformation.name) return true;
|
|
if (currentUserInformation.email != userInformation.email) return true;
|
|
if (currentUserInformation.profile_picture != userInformation.profile_picture) return true;
|
|
if (currentUserInformation.orgs.Length != userInformation.orgs.Length) return true;
|
|
if (currentUserInformation.orgs.Length <= 0) return false;
|
|
for (var i = 0; i < currentUserInformation.orgs.Length; i++)
|
|
{
|
|
var o1 = currentUserInformation.orgs[i];
|
|
var o2 = userInformation.orgs[i];
|
|
if (o1.id != o2.id) return true;
|
|
if (o1.name != o2.name) return true;
|
|
if (o1.canWrite != o2.canWrite) return true;
|
|
if (o1.roles.Length != o2.roles.Length) return true;
|
|
if (o1.roles.Length <= 0) return false;
|
|
for (var j = 0; j < o1.roles.Length; j++)
|
|
{
|
|
if (o1.roles[j] != o2.roles[j]) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// model: get/deployments
|
|
internal class UserDeployments
|
|
{
|
|
public Deployment[] deployments;
|
|
|
|
internal class Deployment
|
|
{
|
|
public string name;
|
|
public string id;
|
|
public string org_id;
|
|
public string url;
|
|
public string updated_at;
|
|
[CanBeNull] public string editUrl;
|
|
}
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
internal static class Authentication
|
|
{
|
|
[InitializeOnLoadMethod]
|
|
private static async void Init()
|
|
{
|
|
// After a domain reload, wait while the editor is updating or compiling
|
|
do await Task.Delay(TimeSpan.FromSeconds(0.2));
|
|
while (EditorApplication.isUpdating || EditorApplication.isCompiling);
|
|
|
|
// For debugging. Requires a domain reload to take effect
|
|
if (NeedleDebug.IsEnabled(TracingScenario.AuthenticationState))
|
|
{
|
|
UserInfoChanged += () =>
|
|
Debug.Log(
|
|
$"<b>User info changed: {_userInformation?.name ?? "null"} ({_userInformation?.profile_picture ?? "no profile picture"})</b>");
|
|
SelectedTeamChanged += () =>
|
|
Debug.Log($"<b>Selected team changed: {SelectedTeamName} ({SelectedTeam})</b>");
|
|
LicenseChanged += () => Debug.Log($"<b>License changed: {LicenseCheck.LastLicenseTypeResult}</b>");
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
LicenseChanged += UnityEditorInternal.InternalEditorUtility.RepaintAllViews;
|
|
#endif
|
|
|
|
#pragma warning disable CS0162
|
|
if (!UsePeriodicLicenseCheck)
|
|
{
|
|
if(ExportInfo.Get())
|
|
Update();
|
|
return;
|
|
}
|
|
#pragma warning restore CS0162
|
|
|
|
// Continuously check for license updates
|
|
while (true)
|
|
{
|
|
if(ExportInfo.Get())
|
|
Update();
|
|
await Task.Delay(TimeSpan.FromSeconds(30));
|
|
}
|
|
}
|
|
|
|
private static DateTime lastLicenseCheckTime;
|
|
|
|
private static async void Update(bool force = false)
|
|
{
|
|
if (!force && DateTime.Now - lastLicenseCheckTime < TimeSpan.FromSeconds(LicenseCheckIntervalSeconds))
|
|
return;
|
|
|
|
lastLicenseCheckTime = DateTime.Now;
|
|
|
|
await GetUserInformation();
|
|
await LicenseCheck.QueryLicense(false);
|
|
}
|
|
|
|
internal static async Task<bool> StartLicenseFlow_Pro()
|
|
{
|
|
var cmd = Authentication.baseCommand + " get-pro";
|
|
var selectedTeam = Authentication.SelectedTeam;
|
|
if (selectedTeam != null) cmd += " --org " + selectedTeam;
|
|
return await ProcessHelper.RunCommand(cmd, null);
|
|
}
|
|
|
|
internal static async Task<bool> StartLicenseFlow_Indie() =>
|
|
await ProcessHelper.RunCommand(Authentication.baseCommand + " get-indie", null);
|
|
|
|
internal static Action SelectedTeamChanged;
|
|
internal static Action UserInfoChanged;
|
|
internal static Action LicenseChanged;
|
|
|
|
internal static string SelectedTeam
|
|
{
|
|
get => EditorPrefs.GetString("Needle_Cloud_Team_ID", null);
|
|
set
|
|
{
|
|
var current = SelectedTeam;
|
|
if (value != current)
|
|
{
|
|
EditorPrefs.SetString("Needle_Cloud_Team_ID", value);
|
|
SelectedTeamChanged?.Invoke();
|
|
LicenseCheck.QueryLicense(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static string SelectedTeamName
|
|
{
|
|
get
|
|
{
|
|
if (SelectedTeam == null || team_ids == null) return null;
|
|
var index = Array.IndexOf(team_ids, SelectedTeam);
|
|
if (index == -1) return null;
|
|
return team_names[index];
|
|
}
|
|
}
|
|
|
|
internal static string[] team_ids;
|
|
internal static string[] team_names;
|
|
|
|
internal static UserInformation _userInformation { get; private set; }
|
|
internal static string _userInformationError { get; private set; }
|
|
|
|
internal static string baseCommand =>
|
|
NeedleDebug.DeveloperMode ? "needle-cloud" : "npx --yes needle-cloud@version-1";
|
|
|
|
private static Task<bool> serverStartCommand;
|
|
private static DateTime lastServerStartTime = DateTime.MinValue;
|
|
|
|
[MenuItem("Needle Engine/Internal/🔨 Restart Licensing Server", false, -1000)]
|
|
private static async void RestartServer()
|
|
{
|
|
await EnsureServerIsRunning(true, true, true);
|
|
}
|
|
|
|
private static int npmCacheErrorCount = 0;
|
|
internal static async Task<bool> EnsureServerIsRunning(bool force = false,
|
|
bool restart = false,
|
|
bool allowLogs = false
|
|
)
|
|
{
|
|
if (!force)
|
|
{
|
|
if (DateTime.Now - lastServerStartTime > TimeSpan.FromSeconds(10))
|
|
{
|
|
}
|
|
else if (serverStartCommand != null)
|
|
return await serverStartCommand;
|
|
else if (DateTime.Now - lastServerStartTime < TimeSpan.FromSeconds(5))
|
|
return true;
|
|
}
|
|
lastServerStartTime = DateTime.Now;
|
|
|
|
while (EditorApplication.isUpdating || EditorApplication.isCompiling)
|
|
{
|
|
await Task.Delay(100);
|
|
}
|
|
|
|
var cmd = $"{baseCommand} start-server --integration unity";
|
|
if (restart) cmd += " --restart";
|
|
var npmErrorType = default(string);
|
|
var foundNpmCacheError = false;
|
|
// var source = new CancellationTokenSource(TimeSpan.FromSeconds(20));
|
|
var options = new ProcessHelper.RunOptions()
|
|
{
|
|
showWindow = false,
|
|
silent = true,
|
|
onLog = (_, log) =>
|
|
{
|
|
if (log != null && log.StartsWith("npm error 404"))
|
|
npmErrorType = "Package not found on NPM → make sure you're on https://registry.npmjs.org/";
|
|
NeedleDebug.Log(TracingScenario.AuthenticationState, "Start server log: " + log);
|
|
}
|
|
};
|
|
serverStartCommand = ProcessHelper.RunCommand(cmd, null, options).ContinueWith(t =>
|
|
{
|
|
var result = t.Result;
|
|
if (!result.success && result.npmCacheLines?.Count > 0)
|
|
{
|
|
// see https://linear.app/needle/issue/NE-6058
|
|
NeedleDebug.Log(TracingScenario.AuthenticationState,
|
|
$"Npm cache error detected: trying to auto-repair");
|
|
var _deleteNpmDirectoriesFromLogs = Tools.DeleteNpmDirectoriesFromLogs(result.npmCacheLines, true);
|
|
npmCacheErrorCount += 1;
|
|
foundNpmCacheError = true;
|
|
}
|
|
return result.success;
|
|
}, TaskScheduler.FromCurrentSynchronizationContext());
|
|
|
|
var startedServer = await serverStartCommand;
|
|
NeedleDebug.Log(TracingScenario.AuthenticationState, "Server started? " + startedServer);
|
|
|
|
// Try to reach the local server a couple of times. We try a maximum of 5 times
|
|
var lastException = default(Exception);
|
|
var canReachServer = false;
|
|
for (var i = 0; i < 5; i++)
|
|
{
|
|
if (foundNpmCacheError)
|
|
{
|
|
// No need to try to ping in this round if we have found a cache issue
|
|
break;
|
|
}
|
|
try
|
|
{
|
|
NeedleDebug.Log(TracingScenario.AuthenticationState, "Pinging server, attempt " + (i + 1));
|
|
var ping = await client
|
|
.GetAsync(localServerUrl)
|
|
.ContinueWith(x => x.Result);
|
|
if (ping.IsSuccessStatusCode)
|
|
{
|
|
lastException = null;
|
|
canReachServer = true;
|
|
NeedleDebug.Log(TracingScenario.AuthenticationState, "Pinged server ✓");
|
|
break;
|
|
}
|
|
}
|
|
catch (ObjectDisposedException e)
|
|
{
|
|
NeedleDebug.LogWarning(TracingScenario.AuthenticationState, e);
|
|
lastException = e;
|
|
_client = new HttpClient();
|
|
}
|
|
catch (TaskCanceledException e)
|
|
{
|
|
NeedleDebug.LogWarning(TracingScenario.AuthenticationState, e);
|
|
lastException = e;
|
|
}
|
|
catch (SocketException e)
|
|
{
|
|
NeedleDebug.LogWarning(TracingScenario.AuthenticationState, e);
|
|
_client = new HttpClient();
|
|
}
|
|
catch (AggregateException e)
|
|
{
|
|
// check if there is a SocketConnection somewhere
|
|
if (e.InnerExceptions.Any(x => x is SocketException))
|
|
{
|
|
NeedleDebug.LogWarning(TracingScenario.AuthenticationState, e);
|
|
_client = new HttpClient();
|
|
}
|
|
else
|
|
{
|
|
NeedleDebug.LogWarning(TracingScenario.AuthenticationState, e);
|
|
lastException = e;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
NeedleDebug.LogWarning(TracingScenario.AuthenticationState, e);
|
|
lastException = e;
|
|
}
|
|
|
|
try
|
|
{
|
|
// ContinueWith is here so that TaskCancelException isn't logged
|
|
await Task.Delay(500).ContinueWith(_ => { });
|
|
}
|
|
// there will be another task running already
|
|
catch (TaskCanceledException e)
|
|
{
|
|
NeedleDebug.LogWarning(TracingScenario.AuthenticationState, e);
|
|
lastException = e;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Wait a moment for deleting cache directories
|
|
if (foundNpmCacheError)
|
|
{
|
|
await Task.Delay(1000);
|
|
}
|
|
|
|
serverStartCommand = null;
|
|
|
|
if (!canReachServer)
|
|
{
|
|
if (foundNpmCacheError && npmCacheErrorCount <= 1)
|
|
{
|
|
// if we detected a server issue give it a little bit of time to self heal
|
|
NeedleDebug.LogWarning(TracingScenario.AuthenticationState, "Restarting license server due to npm cache error. Trying again in a few seconds.");
|
|
await Task.Delay(6000);
|
|
return false;
|
|
}
|
|
|
|
// if(SessionState.GetBool("Needle_LicenseServerSelfHealing"))
|
|
throw new ApplicationException(
|
|
$"Can't reach local Needle License Server on port 8424 ({npmErrorType}). Please try again or restart Unity. If the issue persists please send a bugreport via the menu item \"Needle Engine/Report a Bug\". \n\n\nInner exception: {lastException}");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static HttpClient _client;
|
|
private static HttpClient client => _client ??= new HttpClient();
|
|
internal const string localServerUrl = "http://localhost:8424";
|
|
private const bool UsePeriodicLicenseCheck = true;
|
|
private const int LicenseCheckIntervalSeconds = 2;
|
|
|
|
public static bool LoginCheckInProgress { get; private set; }
|
|
public static bool UserInfoCheckInProgress { get; private set; }
|
|
|
|
public static async Task<bool> LoginAsync(bool allowPrompt, bool silent = false)
|
|
{
|
|
var cmd = $"{baseCommand} login";
|
|
var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60));
|
|
var res = await ProcessHelper.RunCommand(cmd, null, null, true, !silent,
|
|
cancellationToken: tokenSource.Token);
|
|
UpdateUserInformation();
|
|
LoginCheckInProgress = false;
|
|
return res;
|
|
}
|
|
|
|
public static async Task Logout()
|
|
{
|
|
var cmd = $"{baseCommand} logout --headless";
|
|
var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60));
|
|
var res = await ProcessHelper.RunCommand(cmd, null, null, true, true, cancellationToken: tokenSource.Token);
|
|
_userInformation = null;
|
|
UserInfoChanged?.Invoke();
|
|
if (res) Debug.Log("Logged out successfully");
|
|
else Debug.LogError("Failed to logout");
|
|
UpdateUserInformation();
|
|
}
|
|
|
|
public static async Task OpenAccountPage()
|
|
{
|
|
var cmd = $"{baseCommand} open account";
|
|
var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60));
|
|
var res = await ProcessHelper.RunCommand(cmd, null, null, true, false, null, tokenSource.Token);
|
|
if (!res)
|
|
{
|
|
Application.OpenURL("https://cloud.needle.tools/account");
|
|
}
|
|
}
|
|
|
|
internal static bool IsLoggedIn()
|
|
{
|
|
return _userInformation != null;
|
|
}
|
|
|
|
[ItemCanBeNull]
|
|
internal static async Task<LicenseInformation> GetLicenseInformation(CancellationToken token)
|
|
{
|
|
LoginCheckInProgress = true;
|
|
await EnsureServerIsRunning();
|
|
var path = "/api/license?integration=unity";
|
|
var currentTeam = Authentication.SelectedTeam;
|
|
if (!string.IsNullOrWhiteSpace(currentTeam))
|
|
{
|
|
path += "&org=" + currentTeam;
|
|
}
|
|
|
|
HttpResponseMessage res;
|
|
try
|
|
{
|
|
res = await client
|
|
.GetAsync(localServerUrl + path, token)
|
|
.ContinueWith(x => x.Result, token);
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
res = new HttpResponseMessage(HttpStatusCode.NotFound);
|
|
}
|
|
catch (SocketException)
|
|
{
|
|
_client = new HttpClient();
|
|
res = await client.GetAsync(localServerUrl + path, token);
|
|
}
|
|
|
|
if (res.IsSuccessStatusCode)
|
|
{
|
|
var json = await res.Content.ReadAsStringAsync();
|
|
var info = JsonConvert.DeserializeObject<LicenseInformation>(json);
|
|
LoginCheckInProgress = false;
|
|
return info;
|
|
}
|
|
LoginCheckInProgress = false;
|
|
return default;
|
|
}
|
|
|
|
internal static async void UpdateUserInformation() => await GetUserInformation();
|
|
|
|
[ItemCanBeNull]
|
|
internal static async Task<UserInformation> GetUserInformation()
|
|
{
|
|
await EnsureServerIsRunning();
|
|
HttpResponseMessage res;
|
|
UserInfoCheckInProgress = true;
|
|
NeedleDebug.Log(TracingScenario.AuthenticationState, "Checking user information...");
|
|
try
|
|
{
|
|
var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
res = await client.GetAsync(localServerUrl + "/api/user", tokenSource.Token).ContinueWith(x => x.Result, tokenSource.Token);
|
|
}
|
|
catch (SocketException)
|
|
{
|
|
_client = new HttpClient();
|
|
try
|
|
{
|
|
var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
res = await client.GetAsync(localServerUrl + "/api/user", tokenSource.Token)
|
|
.ContinueWith(x => x.Result, tokenSource.Token);
|
|
}
|
|
catch (SocketException)
|
|
{
|
|
// Ignore
|
|
res = new HttpResponseMessage(HttpStatusCode.BadRequest);
|
|
}
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
NeedleDebug.LogWarning(TracingScenario.AuthenticationState, "User information request timed out.");
|
|
return null;
|
|
}
|
|
var currentUserInformation = _userInformation;
|
|
_userInformation = default;
|
|
_userInformationError = default;
|
|
NeedleDebug.Log(TracingScenario.AuthenticationState, "User information response status: " + (int)res.StatusCode);
|
|
if (res.IsSuccessStatusCode)
|
|
{
|
|
var json = await res.Content.ReadAsStringAsync();
|
|
var info = JsonConvert.DeserializeObject<UserInformation>(json);
|
|
_userInformation = info;
|
|
if (_userInformation != null)
|
|
{
|
|
var selected_team = SelectedTeam;
|
|
var count = _userInformation.orgs.Length;
|
|
if (team_ids?.Length == 0 || team_ids?.Length != count)
|
|
{
|
|
team_ids = new string[count];
|
|
team_names = new string[count];
|
|
}
|
|
// team_ids[0] = _userInformation.id;
|
|
// team_names[0] = "Personal";
|
|
for (var i = 0; i < _userInformation.orgs.Length; i++)
|
|
{
|
|
team_ids[i] = _userInformation.orgs[i].id;
|
|
team_names[i] = _userInformation.orgs[i].name;
|
|
}
|
|
|
|
// if the user switched the account and the team id doesnt exist anymore for this user
|
|
if (!team_ids.Contains(selected_team))
|
|
{
|
|
SelectedTeam = team_ids.FirstOrDefault();
|
|
}
|
|
|
|
#pragma warning disable CS4014
|
|
TryGetTexture(_userInformation.id, _userInformation.profile_picture);
|
|
#pragma warning restore CS4014
|
|
}
|
|
else
|
|
{
|
|
team_ids = Array.Empty<string>();
|
|
team_names = Array.Empty<string>();
|
|
}
|
|
|
|
if (UserInformation.NotEqual(currentUserInformation, _userInformation))
|
|
{
|
|
UserInfoChanged?.Invoke();
|
|
#pragma warning disable CS4014
|
|
LicenseCheck.QueryLicense(true);
|
|
#pragma warning restore CS4014
|
|
}
|
|
|
|
UserInfoCheckInProgress = false;
|
|
return info;
|
|
}
|
|
if (res.StatusCode == (HttpStatusCode)403)
|
|
{
|
|
var json = await res.Content.ReadAsStringAsync();
|
|
NeedleDebug.LogWarning(TracingScenario.AuthenticationState, "Failed to get user information: " + res.StatusCode + " - " + json);
|
|
if (json.TrimStart().StartsWith("{"))
|
|
{
|
|
var obj = JObject.Parse(json);
|
|
if (obj.TryGetValue("error", out var errorToken))
|
|
{
|
|
_userInformationError = errorToken.Value<string>();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NeedleDebug.LogWarning(TracingScenario.AuthenticationState, "Failed to get user information: " + res.StatusCode);
|
|
}
|
|
|
|
if (UserInformation.NotEqual(currentUserInformation, _userInformation))
|
|
{
|
|
UserInfoChanged?.Invoke();
|
|
#pragma warning disable CS4014
|
|
LicenseCheck.QueryLicense(true);
|
|
#pragma warning restore CS4014
|
|
}
|
|
|
|
UserInfoCheckInProgress = false;
|
|
return default;
|
|
}
|
|
|
|
internal static async Task<UserDeployments> GetDeployments()
|
|
{
|
|
await EnsureServerIsRunning();
|
|
var res = await client.GetAsync($"{localServerUrl}/api/deployments?org={SelectedTeam}");
|
|
if (res.IsSuccessStatusCode)
|
|
{
|
|
var json = await res.Content.ReadAsStringAsync();
|
|
var info = JsonConvert.DeserializeObject<UserDeployments>(json);
|
|
return info;
|
|
}
|
|
return default;
|
|
}
|
|
|
|
internal static Texture2D TryGetUserProfileTexture()
|
|
{
|
|
if (_userInformation != null)
|
|
{
|
|
if (_textures.TryGetValue(_userInformation.id, out var tex))
|
|
return tex;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static readonly Dictionary<string, Texture2D> _textures = new Dictionary<string, Texture2D>();
|
|
|
|
private static async Task<Texture2D> TryGetTexture(string key, string url)
|
|
{
|
|
{
|
|
if (_textures.TryGetValue(key, out var texture))
|
|
{
|
|
return texture;
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(url))
|
|
{
|
|
var targetDir = "Temp/Needle/.pictures";
|
|
var targetPath = targetDir + "/" + key;
|
|
var firstLoad = false;
|
|
if (!File.Exists(targetPath))
|
|
{
|
|
Directory.CreateDirectory(targetDir);
|
|
var fs = File.Create(targetPath);
|
|
var res = await client.GetByteArrayAsync(url);
|
|
await fs.WriteAsync(res);
|
|
fs.Close();
|
|
await fs.DisposeAsync();
|
|
firstLoad = true;
|
|
}
|
|
if (File.Exists(targetPath))
|
|
{
|
|
var bytes = File.ReadAllBytes(targetPath);
|
|
var texture = new Texture2D(1, 1);
|
|
texture.LoadImage(bytes);
|
|
texture.name = key;
|
|
_textures.TryAdd(key, texture);
|
|
if (firstLoad)
|
|
UserInfoChanged?.Invoke();
|
|
return texture;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
#endif
|
|
} |