diff --git a/UnityProjects/.gitignore b/UnityProjects/.gitignore index f5b5994154f..9a482daae2e 100644 --- a/UnityProjects/.gitignore +++ b/UnityProjects/.gitignore @@ -30,6 +30,9 @@ # Visual Studio Code cache directory .vscode/ +# Rider cache directory +.idea/ + # Gradle cache directory .gradle/ diff --git a/UnityProjects/MRTKDevTemplate/Assets/XR/Settings/OpenXR Package Settings.asset b/UnityProjects/MRTKDevTemplate/Assets/XR/Settings/OpenXR Package Settings.asset index 44a9005260e..ec8c4be060d 100644 --- a/UnityProjects/MRTKDevTemplate/Assets/XR/Settings/OpenXR Package Settings.asset +++ b/UnityProjects/MRTKDevTemplate/Assets/XR/Settings/OpenXR Package Settings.asset @@ -478,11 +478,12 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 9f0ebc320a151d3408ea1e9fce54d40e, type: 3} m_Name: OpenXR Package Settings m_EditorClassIdentifier: - Keys: 010000000e00000007000000 + Keys: 010000000e000000070000000d000000 Values: - {fileID: -7232978043429515688} - {fileID: -3301589181217746260} - {fileID: -5356075571387229412} + - {fileID: 1803865306266780183} --- !u!114 &668207626733321193 MonoBehaviour: m_ObjectHideFlags: 0 @@ -543,6 +544,21 @@ MonoBehaviour: company: Microsoft priority: 0 required: 0 +--- !u!114 &1803865306266780183 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b5a1f07dc5afe854f9f12a4194aca3fb, type: 3} + m_Name: WebGL + m_EditorClassIdentifier: + features: [] + m_renderMode: 1 + m_depthSubmissionMode: 0 --- !u!114 &2243842767272741898 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/UnityProjects/MRTKDevTemplate/Packages/manifest.json b/UnityProjects/MRTKDevTemplate/Packages/manifest.json index b45e02b3a0b..528df20c879 100644 --- a/UnityProjects/MRTKDevTemplate/Packages/manifest.json +++ b/UnityProjects/MRTKDevTemplate/Packages/manifest.json @@ -6,6 +6,7 @@ "com.microsoft.mixedreality.visualprofiler": "https://github.com/microsoft/VisualProfiler-Unity.git#v2.2.0", "com.microsoft.mrtk.accessibility": "file:../../../com.microsoft.mrtk.accessibility", "com.microsoft.mrtk.audio": "file:../../../com.microsoft.mrtk.audio", + "com.microsoft.mrtk.buildwindow": "file:../../../com.microsoft.mrtk.buildwindow", "com.microsoft.mrtk.core": "file:../../../com.microsoft.mrtk.core", "com.microsoft.mrtk.data": "file:../../../com.microsoft.mrtk.data", "com.microsoft.mrtk.diagnostics": "file:../../../com.microsoft.mrtk.diagnostics", diff --git a/UnityProjects/MRTKDevTemplate/Packages/packages-lock.json b/UnityProjects/MRTKDevTemplate/Packages/packages-lock.json index 73e66495078..3f54c63c1ba 100644 --- a/UnityProjects/MRTKDevTemplate/Packages/packages-lock.json +++ b/UnityProjects/MRTKDevTemplate/Packages/packages-lock.json @@ -58,6 +58,12 @@ "com.microsoft.mrtk.core": "3.0.0-development" } }, + "com.microsoft.mrtk.buildwindow": { + "version": "file:../../../com.microsoft.mrtk.buildwindow", + "depth": 0, + "source": "local", + "dependencies": {} + }, "com.microsoft.mrtk.core": { "version": "file:../../../com.microsoft.mrtk.core", "depth": 0, diff --git a/UnityProjects/MRTKDevTemplate/ProjectSettings/ProjectSettings.asset b/UnityProjects/MRTKDevTemplate/ProjectSettings/ProjectSettings.asset index 942fb689015..8a823be042d 100644 --- a/UnityProjects/MRTKDevTemplate/ProjectSettings/ProjectSettings.asset +++ b/UnityProjects/MRTKDevTemplate/ProjectSettings/ProjectSettings.asset @@ -156,7 +156,7 @@ PlayerSettings: androidMaxAspectRatio: 2.1 applicationIdentifier: Android: com.Microsoft.MRTK3Sample - Standalone: com.Microsoft.MRTK3Sample + Standalone: com.Microsoft.MRTK3-Sample buildNumber: Standalone: 0 iPhone: 0 diff --git a/com.microsoft.mrtk.buildwindow/AssemblyInfo.cs b/com.microsoft.mrtk.buildwindow/AssemblyInfo.cs new file mode 100644 index 00000000000..8d6af93d0ec --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/AssemblyInfo.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Reflection; + +[assembly: AssemblyProduct("Microsoft® Mixed Reality Toolkit Build Window")] +[assembly: AssemblyCopyright("Copyright © Microsoft Corporation")] + +// The AssemblyVersion attribute is checked-in and is recommended not to be changed often. +// https://docs.microsoft.com/troubleshoot/visualstudio/general/assembly-version-assembly-file-version +// AssemblyFileVersion and AssemblyInformationalVersion are added by pack-upm.ps1 to match the current MRTK build version. +[assembly: AssemblyVersion("3.0.0.0")] diff --git a/com.microsoft.mrtk.buildwindow/AssemblyInfo.cs.meta b/com.microsoft.mrtk.buildwindow/AssemblyInfo.cs.meta new file mode 100644 index 00000000000..1c44e72fd5a --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cd04de1cc836d51438711f0910137444 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.microsoft.mrtk.buildwindow/AsyncCoroutineRunner.cs b/com.microsoft.mrtk.buildwindow/AsyncCoroutineRunner.cs new file mode 100644 index 00000000000..485245ed264 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/AsyncCoroutineRunner.cs @@ -0,0 +1,175 @@ +// MIT License + +// Copyright(c) 2016 Modest Tree Media Inc + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +[assembly: InternalsVisibleTo("Microsoft.MixedReality.Toolkit.Tests.PlayModeTests")] +namespace Microsoft.MixedReality.Toolkit.Utilities +{ + /// + /// This Async Coroutine Runner is just an object to + /// ensure that coroutines run properly with async/await. + /// + /// + /// The object that this MonoBehavior is attached to must be a root object in the + /// scene, as it will be marked as DontDestroyOnLoad (so that when scenes are changed, + /// it will persist instead of being destroyed). The runner will force itself to + /// the root of the scene if it's rooted elsewhere. + /// + [AddComponentMenu("Scripts/MRTK/Core/AsyncCoroutineRunner")] + internal sealed class AsyncCoroutineRunner : MonoBehaviour + { + private static AsyncCoroutineRunner instance; + + private static bool isInstanceRunning = false; + + private static readonly Queue Actions = new Queue(); + + internal static AsyncCoroutineRunner Instance + { + get + { + if (instance == null) + { + instance = FindObjectOfType(); + } + + // FindObjectOfType() only search for objects attached to active GameObjects. The FindObjectOfType(bool includeInactive) variant is not available to Unity 2019.4 and earlier so cannot be used. + // We instead search for GameObject called AsyncCoroutineRunner and see if it has the component attached. + if (instance == null) + { + var instanceGameObject = GameObject.Find("AsyncCoroutineRunner"); + + if (instanceGameObject != null) + { + instance = instanceGameObject.GetComponent(); + + if (instance == null) + { + Debug.Log("[AsyncCoroutineRunner] Found a \"AsyncCoroutineRunner\" GameObject but didn't have the AsyncCoroutineRunner component attached. Attaching the script."); + instance = instanceGameObject.AddComponent(); + } + } + } + + if (instance == null) + { + Debug.Log("[AsyncCoroutineRunner] There is no AsyncCoroutineRunner in the scene. Adding a GameObject with AsyncCoroutineRunner attached at the root of the scene."); + instance = new GameObject("AsyncCoroutineRunner").AddComponent(); + } + else if (!instance.isActiveAndEnabled) + { + if (!instance.enabled) + { + Debug.LogWarning("[AsyncCoroutineRunner] Found a disabled AsyncCoroutineRunner component. Enabling the component."); + instance.enabled = true; + } + if (!instance.gameObject.activeSelf) + { + Debug.LogWarning("[AsyncCoroutineRunner] Found an AsyncCoroutineRunner attached to an inactive GameObject. Setting the GameObject active."); + instance.gameObject.SetActive(true); + } + } + + instance.gameObject.hideFlags = HideFlags.None; + + // AsyncCoroutineRunner must be at the root so that we can call DontDestroyOnLoad on it. + // This is ultimately to ensure that it persists across scene loads/unloads. + if (instance.transform.parent != null) + { + Debug.LogWarning($"[AsyncCoroutineRunner] AsyncCoroutineRunner was found as a child of another GameObject {instance.transform.parent}, " + + "it must be a root object in the scene. Moving the AsyncCoroutineRunner to the root."); + instance.transform.parent = null; + } + +#if !UNITY_EDITOR + DontDestroyOnLoad(instance); +#endif + return instance; + } + } + + internal static void Post(Action task) + { + lock (Actions) + { + Actions.Enqueue(task); + } + } + + internal static bool IsInstanceRunning => isInstanceRunning; + + private void Update() + { + if (Instance != this) + { + Debug.Log("[AsyncCoroutineRunner] Multiple active AsyncCoroutineRunners is present in the scene. Disabling duplicate ones."); + enabled = false; + return; + } + isInstanceRunning = true; + + int actionCount; + + lock (Actions) + { + actionCount = Actions.Count; + } + + for (int i = 0; i < actionCount; i++) + { + Action next; + + lock (Actions) + { + next = Actions.Dequeue(); + } + + next(); + } + } + + private void OnDisable() + { + if (instance == this) + { + isInstanceRunning = false; + } + } + + private void OnEnable() + { + if (Instance != this) + { + Debug.Log("[AsyncCoroutineRunner] Multiple active AsyncCoroutineRunners is present in the scene. Disabling duplicate ones."); + enabled = false; + } + else + { + isInstanceRunning = true; + } + } + } +} diff --git a/com.microsoft.mrtk.buildwindow/AsyncCoroutineRunner.cs.meta b/com.microsoft.mrtk.buildwindow/AsyncCoroutineRunner.cs.meta new file mode 100644 index 00000000000..44fb96f1828 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/AsyncCoroutineRunner.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3bcd4a2fcc384f158dcbe58dcac06f64 +timeCreated: 1663172198 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy.meta new file mode 100644 index 00000000000..027a685d2ed --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b7605e91495148abb508e5e56e62f9d8 +timeCreated: 1663169360 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildDeployPreferences.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildDeployPreferences.cs new file mode 100644 index 00000000000..7a76fbc70cc --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildDeployPreferences.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.MixedReality.Toolkit.Utilities.Editor; +using System; +using System.IO; +using UnityEditor; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.Build.Editor +{ + /// + /// Build and Deploy Specific Editor Preferences for the Build and Deploy Window. + /// + public static class BuildDeployPreferences + { + // Constants + private const string EDITOR_PREF_BUILD_DIR = "BuildDeployWindow_BuildDir"; + private const string EDITOR_PREF_INCREMENT_BUILD_VERSION = "BuildDeployWindow_IncrementBuildVersion"; + private const string EDITOR_PREF_3D_APP_LAUNCHER_MODEL_LOCATION = "BuildDeployWindow_AppLauncherModelLocation"; + + /// + /// The Build Directory that the Mixed Reality Toolkit will build to. + /// + /// + /// This is a root build folder path. Each platform build will be put into a child directory with the name of the current active build target. + /// + public static string BuildDirectory + { + get => $"{EditorPreferences.Get(EDITOR_PREF_BUILD_DIR, "Builds")}/{EditorUserBuildSettings.activeBuildTarget}"; + set => EditorPreferences.Set(EDITOR_PREF_BUILD_DIR, value.Replace($"/{EditorUserBuildSettings.activeBuildTarget}", string.Empty)); + } + + /// + /// The absolute path to + /// + public static string AbsoluteBuildDirectory + { + get + { + string rootBuildDirectory = BuildDirectory; + int dirCharIndex = rootBuildDirectory.IndexOf("/", StringComparison.Ordinal); + + if (dirCharIndex != -1) + { + rootBuildDirectory = rootBuildDirectory.Substring(0, dirCharIndex); + } + + return Path.GetFullPath(Path.Combine(Path.Combine(Application.dataPath, ".."), rootBuildDirectory)); + } + } + + /// + /// Current setting to increment build visioning. + /// + public static bool IncrementBuildVersion + { + get => EditorPreferences.Get(EDITOR_PREF_INCREMENT_BUILD_VERSION, true); + set => EditorPreferences.Set(EDITOR_PREF_INCREMENT_BUILD_VERSION, value); + } + + /// + /// The location in Assets of the 3D app launcher model for an AppX build. + /// + /// See 3D app launcher design guidance for more information. + public static string AppLauncherModelLocation + { + get => ProjectPreferences.Get(EDITOR_PREF_3D_APP_LAUNCHER_MODEL_LOCATION, string.Empty); + set => ProjectPreferences.Set(EDITOR_PREF_3D_APP_LAUNCHER_MODEL_LOCATION, value); + } + } +} diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildDeployPreferences.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildDeployPreferences.cs.meta new file mode 100644 index 00000000000..c1ddd5c3e40 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildDeployPreferences.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4d89548688b44f84850275a398d097c8 +timeCreated: 1663169360 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfo.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfo.cs new file mode 100644 index 00000000000..70e3e1a5a24 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfo.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEditor.Build.Reporting; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.Build.Editor +{ + public class BuildInfo : IBuildInfo + { + public BuildInfo(bool isCommandLine = false) + { + IsCommandLine = isCommandLine; + BuildSymbols = string.Empty; + BuildTarget = EditorUserBuildSettings.activeBuildTarget; + Scenes = EditorBuildSettings.scenes.Where(scene => scene.enabled).Select(scene => scene.path); + } + + /// + public virtual BuildTarget BuildTarget { get; } + + /// + public bool IsCommandLine { get; } + + private string outputDirectory; + + /// + public string OutputDirectory + { + get => string.IsNullOrEmpty(outputDirectory) ? outputDirectory = BuildDeployPreferences.BuildDirectory : outputDirectory; + set => outputDirectory = value; + } + + /// + public IEnumerable Scenes { get; set; } + + /// + public Action PreBuildAction { get; set; } + + /// + public Action PostBuildAction { get; set; } + + /// + public BuildOptions BuildOptions { get; set; } + + /// + public ColorSpace? ColorSpace { get; set; } + + /// + public ScriptingImplementation? ScriptingBackend { get; set; } + + /// + public bool AutoIncrement { get; set; } = false; + + /// + public string BuildSymbols { get; set; } + + /// + public string BuildPlatform { get; set; } + + /// + public string Configuration + { + get + { + if (!this.HasConfigurationSymbol()) + { + return UnityPlayerBuildTools.BuildSymbolMaster; + } + + return this.HasAnySymbols(UnityPlayerBuildTools.BuildSymbolDebug) + ? UnityPlayerBuildTools.BuildSymbolDebug + : this.HasAnySymbols(UnityPlayerBuildTools.BuildSymbolRelease) + ? UnityPlayerBuildTools.BuildSymbolRelease + : UnityPlayerBuildTools.BuildSymbolMaster; + } + set + { + if (this.HasConfigurationSymbol()) + { + this.RemoveSymbols(new[] + { + UnityPlayerBuildTools.BuildSymbolDebug, + UnityPlayerBuildTools.BuildSymbolRelease, + UnityPlayerBuildTools.BuildSymbolMaster + }); + } + + this.AppendSymbols(value); + } + } + + /// + public string LogDirectory { get; set; } + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfo.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfo.cs.meta new file mode 100644 index 00000000000..f4a5228d341 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b23ec68cb2414db4ae5ddb5f0961e041 +timeCreated: 1663169360 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfoExtensions.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfoExtensions.cs new file mode 100644 index 00000000000..7fd2ae461af --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfoExtensions.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using UnityEditor; + +namespace Microsoft.MixedReality.Toolkit.Build.Editor +{ + public static class BuildInfoExtensions + { + /// + /// Append symbols to the end of the 's. + /// + /// The string array to append. + public static void AppendSymbols(this IBuildInfo buildInfo, params string[] symbol) + { + buildInfo.AppendSymbols((IEnumerable)symbol); + } + + /// + /// Append symbols to the end of the 's . + /// + /// The string collection to append. + public static void AppendSymbols(this IBuildInfo buildInfo, IEnumerable symbols) + { + string[] toAdd = symbols.Except(buildInfo.BuildSymbols.Split(';')) + .Where(symbol => !string.IsNullOrEmpty(symbol)).ToArray(); + + if (!toAdd.Any()) + { + return; + } + + if (!string.IsNullOrEmpty(buildInfo.BuildSymbols)) + { + buildInfo.BuildSymbols += ";"; + } + + buildInfo.BuildSymbols += string.Join(";", toAdd); + } + + /// + /// Remove symbols from the 's . + /// + /// The string collection to remove. + public static void RemoveSymbols(this IBuildInfo buildInfo, IEnumerable symbolsToRemove) + { + string[] toKeep = buildInfo.BuildSymbols.Split(';').Except(symbolsToRemove).ToArray(); + + if (!toKeep.Any()) + { + return; + } + + if (!string.IsNullOrEmpty(buildInfo.BuildSymbols)) + { + buildInfo.BuildSymbols = string.Empty; + } + + buildInfo.BuildSymbols += string.Join(";", toKeep); + } + + /// + /// Does the contain any of the provided symbols in the ? + /// + /// The string array of symbols to match. + /// True, if any of the provided symbols are in the + public static bool HasAnySymbols(this IBuildInfo buildInfo, params string[] symbols) + { + if (string.IsNullOrEmpty(buildInfo.BuildSymbols)) { return false; } + + return buildInfo.BuildSymbols.Split(';').Intersect(symbols).Any(); + } + + /// + /// Does the contain any of the provided symbols in the ? + /// + /// The string collection of symbols to match. + /// True, if any of the provided symbols are in the + public static bool HasAnySymbols(this IBuildInfo buildInfo, IEnumerable symbols) + { + if (string.IsNullOrEmpty(buildInfo.BuildSymbols)) { return false; } + + return buildInfo.BuildSymbols.Split(';').Intersect(symbols).Any(); + } + + /// + /// Checks if the has any configuration symbols (i.e. debug, release, or master). + /// + /// True, if the contains debug, release, or master. + public static bool HasConfigurationSymbol(this IBuildInfo buildInfo) + { + return buildInfo.HasAnySymbols( + UnityPlayerBuildTools.BuildSymbolDebug, + UnityPlayerBuildTools.BuildSymbolRelease, + UnityPlayerBuildTools.BuildSymbolMaster); + } + + /// + /// Appends the 's without including debug, release or master. + /// + /// Symbols to append. + public static void AppendWithoutConfigurationSymbols(this IBuildInfo buildInfo, string symbols) + { + buildInfo.AppendSymbols(symbols.Split(';').Except(new[] + { + UnityPlayerBuildTools.BuildSymbolDebug, + UnityPlayerBuildTools.BuildSymbolRelease, + UnityPlayerBuildTools.BuildSymbolMaster + }).ToArray()); + } + + /// + /// Gets the BuildTargetGroup for the 's BuildTarget + /// + /// The BuildTargetGroup for the 's BuildTarget + public static BuildTargetGroup GetGroup(this BuildTarget buildTarget) + { + switch (buildTarget) + { + case BuildTarget.WSAPlayer: + return BuildTargetGroup.WSA; + case BuildTarget.StandaloneWindows: + case BuildTarget.StandaloneWindows64: + return BuildTargetGroup.Standalone; + default: + return BuildTargetGroup.Unknown; + } + } + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfoExtensions.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfoExtensions.cs.meta new file mode 100644 index 00000000000..ba0ecab2b3f --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/BuildInfoExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e365115c037641efb578a2cc713156ab +timeCreated: 1663169360 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/IBuildInfo.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/IBuildInfo.cs new file mode 100644 index 00000000000..bad5de5ea07 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/IBuildInfo.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEditor.Build.Reporting; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.Build.Editor +{ + /// + /// The Build Info defines common properties for a build. + /// + public interface IBuildInfo + { + /// + /// Is this build being issued from the command line? + /// + bool IsCommandLine { get; } + + /// + /// The directory to put the final build output. + /// + /// + /// Defaults to "Application.dataPath/Builds/Platform Target/" + /// + string OutputDirectory { get; set; } + + /// + /// The list of scenes to include in the build. + /// + IEnumerable Scenes { get; set; } + + /// + /// A pre-build action to raise before building the Unity player. + /// + Action PreBuildAction { get; set; } + + /// + /// A post-build action to raise after building the Unity player. + /// + Action PostBuildAction { get; set; } + + /// + /// Build options to include in the Unity player build pipeline. + /// + BuildOptions BuildOptions { get; set; } + + /// + /// The build target. + /// + BuildTarget BuildTarget { get; } + + /// + /// Optional parameter to set the player's + /// + ColorSpace? ColorSpace { get; set; } + + /// + /// Optional parameter to set the scripting backend + /// + ScriptingImplementation? ScriptingBackend { get; set; } + + /// + /// Should the build auto increment the build version number? + /// + bool AutoIncrement { get; set; } + + /// + /// The symbols associated with this build. + /// + string BuildSymbols { get; set; } + + /// + /// The build configuration (i.e. debug, release, or master) + /// + string Configuration { get; set; } + + /// + /// The build platform (i.e. x86, x64) + /// + string BuildPlatform { get; set; } + + /// + /// The default location of log files generated by sub-processes of the build system. + /// + /// + /// Note that this different from the Unity flag -logFile, which controls the location + /// of the Unity log file. This is specifically for logs generated by other processes + /// that the MRTK build tools produces (for example, when msbuild.exe is involved) + /// + string LogDirectory { get; set; } + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/IBuildInfo.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/IBuildInfo.cs.meta new file mode 100644 index 00000000000..a3b2989fe92 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/IBuildInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 886f0edadb5c41269107dd3159a7a065 +timeCreated: 1663169360 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/MixedRealityBuildPreferences.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/MixedRealityBuildPreferences.cs new file mode 100644 index 00000000000..594edd93451 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/MixedRealityBuildPreferences.cs @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.IO; +using System.Xml; +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.Build.Editor +{ + using GLTFast; + + /// + /// Settings provider for build-specific settings, like the 3D app launcher model for Windows builds. + /// + public class MixedRealityBuildPreferences : IPreprocessBuildWithReport, IPostprocessBuildWithReport + { + private const string AppLauncherPath = @"Assets\AppLauncherModel.glb"; + private static readonly GUIContent AppLauncherModelLabel = new GUIContent("3D App Launcher Model", "Location of .glb model to use as a 3D App Launcher"); + private static UnityEditor.Editor gameObjectEditor = null; + private static GUIStyle appLauncherPreviewBackgroundColor = null; + private static bool isBuilding = false; + + // Arbitrary callback order, chosen to be larger so that it runs after other things that + // a developer may have already. + int IOrderedCallback.callbackOrder => 100; + + [SettingsProvider] + private static SettingsProvider BuildPreferences() + { + var provider = new SettingsProvider("Project/Mixed Reality Toolkit/Build Settings", SettingsScope.Project) + { + guiHandler = GUIHandler, + + keywords = new HashSet(new[] { "Mixed", "Reality", "Toolkit", "Build" }) + }; + + void GUIHandler(string searchContext) + { + EditorGUILayout.HelpBox("These settings are serialized into ProjectPreferences.asset in the MixedRealityToolkit-Generated folder.\nThis file can be checked into source control to maintain consistent settings across collaborators.", MessageType.Info); + DrawAppLauncherModelField(); + } + + return provider; + } + + /// + /// Helper script for rendering an object field to set the 3D app launcher model in an editor window. + /// + /// See 3D app launcher design guidance for more information. + public static void DrawAppLauncherModelField(bool showInteractivePreview = true) + { + using (new EditorGUILayout.HorizontalScope()) + { + GltfAsset newGlbModel; + bool appLauncherChanged = false; + + // 3D launcher model + string curAppLauncherModelLocation = BuildDeployPreferences.AppLauncherModelLocation; + var curGlbModel = AssetDatabase.LoadAssetAtPath(curAppLauncherModelLocation, typeof(GltfAsset)); + + using (new EditorGUILayout.VerticalScope()) + { + EditorGUILayout.LabelField(AppLauncherModelLabel); + newGlbModel = EditorGUILayout.ObjectField(curGlbModel, typeof(GltfAsset), false, GUILayout.MaxWidth(256)) as GltfAsset; + string newAppLauncherModelLocation = AssetDatabase.GetAssetPath(newGlbModel); + if (newAppLauncherModelLocation != curAppLauncherModelLocation) + { + BuildDeployPreferences.AppLauncherModelLocation = newAppLauncherModelLocation; + appLauncherChanged = true; + } + } + + // The preview GUI has a problem during the build, so we don't render it + if (newGlbModel != null && newGlbModel.sceneInstance != null && showInteractivePreview && !isBuilding) + { + if (gameObjectEditor == null || appLauncherChanged) + { + gameObjectEditor = UnityEditor.Editor.CreateEditor(newGlbModel.gameObject); + } + + if (appLauncherPreviewBackgroundColor == null) + { + appLauncherPreviewBackgroundColor = new GUIStyle(); + appLauncherPreviewBackgroundColor.normal.background = EditorGUIUtility.whiteTexture; + } + + gameObjectEditor.OnInteractivePreviewGUI(GUILayoutUtility.GetRect(128, 128), appLauncherPreviewBackgroundColor); + } + } + } + + void IPreprocessBuildWithReport.OnPreprocessBuild(BuildReport report) + { + if (report.summary.platformGroup == BuildTargetGroup.WSA && !string.IsNullOrEmpty(BuildDeployPreferences.AppLauncherModelLocation)) + { + isBuilding = true; + // Sets the editor to null. On a build, Unity reloads the object preview + // in a seemingly unexpected way, so it starts rendering a null texture. + // This refreshes the preview window instead. + gameObjectEditor = null; + } + } + + void IPostprocessBuildWithReport.OnPostprocessBuild(BuildReport report) + { + if (report.summary.platformGroup == BuildTargetGroup.WSA && !string.IsNullOrEmpty(BuildDeployPreferences.AppLauncherModelLocation)) + { + string appxPath = $"{report.summary.outputPath}/{PlayerSettings.productName}"; + + Debug.Log($"3D App Launcher: {BuildDeployPreferences.AppLauncherModelLocation}, Destination: {appxPath}/{AppLauncherPath}"); + + FileUtil.ReplaceFile(BuildDeployPreferences.AppLauncherModelLocation, $"{appxPath}/{AppLauncherPath}"); + AddAppLauncherModelToProject($"{appxPath}/{PlayerSettings.productName}.vcxproj"); + AddAppLauncherModelToFilter($"{appxPath}/{PlayerSettings.productName}.vcxproj.filters"); + UpdateManifest($"{appxPath}/Package.appxmanifest"); + + isBuilding = false; + } + } + + private static void AddAppLauncherModelToProject(string filePath) + { + var text = File.ReadAllText(filePath); + var doc = new XmlDocument(); + doc.LoadXml(text); + var root = doc.DocumentElement; + + // Check to see if model has already been added + XmlNodeList nodes = root.SelectNodes($"//None[@Include = \"{AppLauncherPath}\"]"); + if (nodes.Count > 0) + { + return; + } + + var newNodeDoc = new XmlDocument(); + newNodeDoc.LoadXml($"" + + "true" + + ""); + var newNode = doc.ImportNode(newNodeDoc.DocumentElement, true); + var list = doc.GetElementsByTagName("ItemGroup"); + var items = list.Item(1); + items.AppendChild(newNode); + doc.Save(filePath); + } + + private static void AddAppLauncherModelToFilter(string filePath) + { + var text = File.ReadAllText(filePath); + var doc = new XmlDocument(); + doc.LoadXml(text); + var root = doc.DocumentElement; + + // Check to see if model has already been added + XmlNodeList nodes = root.SelectNodes($"//None[@Include = \"{AppLauncherPath}\"]"); + if (nodes.Count > 0) + { + return; + } + + var newNodeDoc = new XmlDocument(); + newNodeDoc.LoadXml($"" + + "Assets" + + ""); + var newNode = doc.ImportNode(newNodeDoc.DocumentElement, true); + var list = doc.GetElementsByTagName("ItemGroup"); + var items = list.Item(0); + items.AppendChild(newNode); + doc.Save(filePath); + } + + private static void UpdateManifest(string filePath) + { + var text = File.ReadAllText(filePath); + var doc = new XmlDocument(); + doc.LoadXml(text); + var root = doc.DocumentElement; + + // Check to see if the element exists already + XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable); + nsmgr.AddNamespace("uap5", "http://schemas.microsoft.com/appx/manifest/uap/windows10/5"); + XmlNodeList nodes = root.SelectNodes("//uap5:MixedRealityModel", nsmgr); + foreach (XmlNode node in nodes) + { + if (node.Attributes != null && node.Attributes["Path"].Value == AppLauncherPath) + { + return; + } + } + root.SetAttribute("xmlns:uap5", "http://schemas.microsoft.com/appx/manifest/uap/windows10/5"); + + var ignoredValue = root.GetAttribute("IgnorableNamespaces"); + root.SetAttribute("IgnorableNamespaces", ignoredValue + " uap5"); + + var newElement = doc.CreateElement("uap5", "MixedRealityModel", "http://schemas.microsoft.com/appx/manifest/uap/windows10/5"); + newElement.SetAttribute("Path", AppLauncherPath); + var list = doc.GetElementsByTagName("uap:DefaultTile"); + var items = list.Item(0); + items.AppendChild(newElement); + + doc.Save(filePath); + } + } +} diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/MixedRealityBuildPreferences.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/MixedRealityBuildPreferences.cs.meta new file mode 100644 index 00000000000..419cc5ef032 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/MixedRealityBuildPreferences.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6de56e5071ad49b297f4bcbd5194fe14 +timeCreated: 1663169360 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UnityPlayerBuildTools.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UnityPlayerBuildTools.cs new file mode 100644 index 00000000000..31441cbb338 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UnityPlayerBuildTools.cs @@ -0,0 +1,312 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.MixedReality.Toolkit.Utilities.Editor; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using UnityEditor; +using UnityEditor.Build.Reporting; +using UnityEngine; +using Debug = UnityEngine.Debug; + +namespace Microsoft.MixedReality.Toolkit.Build.Editor +{ + /// + /// Cross platform player build tools + /// + public static class UnityPlayerBuildTools + { + // Build configurations. Exactly one of these should be defined for any given build. + public const string BuildSymbolDebug = "debug"; + public const string BuildSymbolRelease = "release"; + public const string BuildSymbolMaster = "master"; + + /// + /// Starts the build process + /// + /// The BuildReport from Unity's BuildPipeline + public static BuildReport BuildUnityPlayer(IBuildInfo buildInfo) + { + EditorUtility.DisplayProgressBar("Build Pipeline", "Gathering Build Data...", 0.25f); + + // Call the pre-build action, if any + buildInfo.PreBuildAction?.Invoke(buildInfo); + + BuildTargetGroup buildTargetGroup = buildInfo.BuildTarget.GetGroup(); + string playerBuildSymbols = PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup); + + if (!string.IsNullOrEmpty(playerBuildSymbols)) + { + if (buildInfo.HasConfigurationSymbol()) + { + buildInfo.AppendWithoutConfigurationSymbols(playerBuildSymbols); + } + else + { + buildInfo.AppendSymbols(playerBuildSymbols.Split(';')); + } + } + + if (!string.IsNullOrEmpty(buildInfo.BuildSymbols)) + { + PlayerSettings.SetScriptingDefineSymbolsForGroup(buildTargetGroup, buildInfo.BuildSymbols); + } + + if ((buildInfo.BuildOptions & BuildOptions.Development) == BuildOptions.Development && + !buildInfo.HasConfigurationSymbol()) + { + buildInfo.AppendSymbols(BuildSymbolDebug); + } + + if (buildInfo.HasAnySymbols(BuildSymbolDebug)) + { + buildInfo.BuildOptions |= BuildOptions.Development | BuildOptions.AllowDebugging; + } + + if (buildInfo.HasAnySymbols(BuildSymbolRelease)) + { + // Unity automatically adds the DEBUG symbol if the BuildOptions.Development flag is + // specified. In order to have debug symbols and the RELEASE symbols we have to + // inject the symbol Unity relies on to enable the /debug+ flag of csc.exe which is "DEVELOPMENT_BUILD" + buildInfo.AppendSymbols("DEVELOPMENT_BUILD"); + } + + var oldColorSpace = PlayerSettings.colorSpace; + + if (buildInfo.ColorSpace.HasValue) + { + PlayerSettings.colorSpace = buildInfo.ColorSpace.Value; + } + + if (buildInfo.ScriptingBackend.HasValue) + { + PlayerSettings.SetScriptingBackend(buildTargetGroup, buildInfo.ScriptingBackend.Value); + } + + BuildTarget oldBuildTarget = EditorUserBuildSettings.activeBuildTarget; + BuildTargetGroup oldBuildTargetGroup = oldBuildTarget.GetGroup(); + + if (EditorUserBuildSettings.activeBuildTarget != buildInfo.BuildTarget) + { + EditorUserBuildSettings.SwitchActiveBuildTarget(buildTargetGroup, buildInfo.BuildTarget); + } + + switch (buildInfo.BuildTarget) + { + case BuildTarget.Android: + buildInfo.OutputDirectory = $"{buildInfo.OutputDirectory}/{PlayerSettings.productName}.apk"; + break; + case BuildTarget.StandaloneWindows: + case BuildTarget.StandaloneWindows64: + buildInfo.OutputDirectory = $"{buildInfo.OutputDirectory}/{PlayerSettings.productName}.exe"; + break; + } + + BuildReport buildReport = default; + + try + { + buildReport = BuildPipeline.BuildPlayer( + buildInfo.Scenes.ToArray(), + buildInfo.OutputDirectory, + buildInfo.BuildTarget, + buildInfo.BuildOptions); + } + catch (Exception e) + { + Debug.LogError($"{e.Message}\n{e.StackTrace}"); + } + + PlayerSettings.colorSpace = oldColorSpace; + + if (EditorUserBuildSettings.activeBuildTarget != oldBuildTarget) + { + EditorUserBuildSettings.SwitchActiveBuildTarget(oldBuildTargetGroup, oldBuildTarget); + } + + // Call the post-build action, if any + buildInfo.PostBuildAction?.Invoke(buildInfo, buildReport); + + EditorUtility.ClearProgressBar(); + + return buildReport; + } + + /// + /// Force Unity To Write Project Files + /// + public static void SyncSolution() + { + var syncVs = Type.GetType("UnityEditor.SyncVS,UnityEditor"); + var syncSolution = syncVs.GetMethod("SyncSolution", BindingFlags.Public | BindingFlags.Static); + syncSolution.Invoke(null, null); + } + + /// + /// Start a build using Unity's command line. + /// + public static async void StartCommandLineBuild() + { + var success = await BuildUnityPlayerSimplified(); + Debug.Log($"Exiting build..."); + EditorApplication.Exit(success ? 0 : 1); + } + + public static async Task BuildUnityPlayerSimplified() + { + // We don't need stack traces on all our logs. Makes things a lot easier to read. + Application.SetStackTraceLogType(LogType.Log, StackTraceLogType.None); + Debug.Log($"Starting command line build for {EditorUserBuildSettings.activeBuildTarget}..."); + EditorAssemblyReloadManager.LockReloadAssemblies = true; + + bool success; + try + { + SyncSolution(); + switch (EditorUserBuildSettings.activeBuildTarget) + { + case BuildTarget.WSAPlayer: + success = await UwpPlayerBuildTools.BuildPlayer(new UwpBuildInfo(true)); + break; + default: + var buildInfo = new BuildInfo(true) as IBuildInfo; + ParseBuildCommandLine(ref buildInfo); + var buildResult = BuildUnityPlayer(buildInfo); + success = buildResult.summary.result == BuildResult.Succeeded; + break; + } + } + catch (Exception e) + { + Debug.LogError($"Build Failed!\n{e.Message}\n{e.StackTrace}"); + success = false; + } + + Debug.Log($"Finished build... Build success? {success}"); + return success; + } + + internal static bool CheckBuildScenes() + { + if (EditorBuildSettings.scenes.Length == 0) + { + return EditorUtility.DisplayDialog("Attention!", + "No scenes are present in the build settings.\n" + + "The current scene will be the one built.\n\n" + + "Do you want to cancel and add one?", + "Continue Anyway", "Cancel Build"); + } + + return true; + } + + /// + /// Get the Unity Project Root Path. + /// + /// The full path to the project's root. + public static string GetProjectPath() + { + return Path.GetDirectoryName(Path.GetFullPath(Application.dataPath)); + } + + public static void ParseBuildCommandLine(ref IBuildInfo buildInfo) + { + string[] arguments = Environment.GetCommandLineArgs(); + + // Boolean used to track whether builfInfo contains scenes that are not specified by command line arguments. + // These non command line arugment scenes should be overwritten by those specified in the command line. + bool buildInfoContainsNonCommandLineScene = buildInfo.Scenes.Count() > 0; + + for (int i = 0; i < arguments.Length; ++i) + { + switch (arguments[i]) + { + case "-autoIncrement": + buildInfo.AutoIncrement = true; + break; + case "-sceneList": + if (buildInfoContainsNonCommandLineScene) + { + buildInfo.Scenes = SplitSceneList(arguments[++i]); + buildInfoContainsNonCommandLineScene = false; + } + else + { + buildInfo.Scenes = buildInfo.Scenes.Union(SplitSceneList(arguments[++i])); + } + break; + case "-sceneListFile": + string path = arguments[++i]; + if (File.Exists(path)) + { + if (buildInfoContainsNonCommandLineScene) + { + buildInfo.Scenes = SplitSceneList(File.ReadAllText(path)); + buildInfoContainsNonCommandLineScene = false; + } + else + { + buildInfo.Scenes = buildInfo.Scenes.Union(SplitSceneList(File.ReadAllText(path))); + } + } + else + { + Debug.LogWarning($"Scene list file at '{path}' does not exist."); + } + break; + case "-buildOutput": + buildInfo.OutputDirectory = arguments[++i]; + break; + case "-colorSpace": + buildInfo.ColorSpace = (ColorSpace)Enum.Parse(typeof(ColorSpace), arguments[++i]); + break; + case "-scriptingBackend": + buildInfo.ScriptingBackend = (ScriptingImplementation)Enum.Parse(typeof(ScriptingImplementation), arguments[++i]); + break; + case "-x86": + case "-x64": + case "-arm": + case "-arm64": + buildInfo.BuildPlatform = arguments[i].Substring(1); + break; + case "-debug": + case "-master": + case "-release": + buildInfo.Configuration = arguments[i].Substring(1).ToLower(); + break; + case "-logDirectory": + buildInfo.LogDirectory = arguments[++i]; + break; + } + } + } + + private static IEnumerable SplitSceneList(string sceneList) + { + return from scene in sceneList.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + select scene.Trim(); + } + + /// + /// Restores any nuget packages at the path specified. + /// + /// True, if the nuget packages were successfully restored. + public static async Task RestoreNugetPackagesAsync(string nugetPath, string storePath) + { + Debug.Assert(File.Exists(nugetPath)); + Debug.Assert(Directory.Exists(storePath)); + + string projectJSONPath = Path.Combine(storePath, "project.json"); + string projectJSONLockPath = Path.Combine(storePath, "project.lock.json"); + + await new Process().StartProcessAsync(nugetPath, $"restore \"{projectJSONPath}\""); + + return File.Exists(projectJSONLockPath); + } + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UnityPlayerBuildTools.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UnityPlayerBuildTools.cs.meta new file mode 100644 index 00000000000..c5ee311a389 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UnityPlayerBuildTools.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 828a69fe27da4beca840ed94a6d2f6e5 +timeCreated: 1663169360 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpAppxBuildTools.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpAppxBuildTools.cs new file mode 100644 index 00000000000..b59f1da3d13 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpAppxBuildTools.cs @@ -0,0 +1,706 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.MixedReality.Toolkit.Utilities.Editor; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using UnityEditor; +using UnityEngine; +using Debug = UnityEngine.Debug; + +namespace Microsoft.MixedReality.Toolkit.Build.Editor +{ + public static class UwpAppxBuildTools + { + /// + /// Query the build process to see if we're already building. + /// + public static bool IsBuilding { get; private set; } = false; + + /// + /// The list of filename extensions that are valid VCProjects. + /// + private static readonly string[] VcProjExtensions = { "vcsproj", "vcxproj" }; + + /// + /// Build the UWP appx bundle for this project. Requires that has already be run or a user has + /// previously built the Unity Player with the WSA Player as the Build Target. + /// + /// True, if the appx build was successful. + public static async Task BuildAppxAsync(UwpBuildInfo buildInfo, CancellationToken cancellationToken = default) + { + if (!EditorAssemblyReloadManager.LockReloadAssemblies) + { + Debug.LogError("Lock Reload assemblies before attempting to build appx!"); + return false; + } + + if (IsBuilding) + { + Debug.LogWarning("Build already in progress!"); + return false; + } + + if (Application.isBatchMode) + { + // We don't need stack traces on all our logs. Makes things a lot easier to read. + Application.SetStackTraceLogType(LogType.Log, StackTraceLogType.None); + } + + Debug.Log("Starting Unity Appx Build..."); + + IsBuilding = true; + string slnFilename = Path.Combine(buildInfo.OutputDirectory, $"{PlayerSettings.productName}.sln"); + + if (!File.Exists(slnFilename)) + { + Debug.LogError("Unable to find Solution to build from!"); + return IsBuilding = false; + } + + // Get and validate the msBuild path... + var msBuildPath = await FindMsBuildPathAsync(); + + if (!File.Exists(msBuildPath)) + { + Debug.LogError($"MSBuild.exe is missing or invalid!\n{msBuildPath}"); + return IsBuilding = false; + } + + // Ensure that the generated .appx version increments by modifying Package.appxmanifest + try + { + if (!UpdateAppxManifest(buildInfo)) + { + throw new Exception(); + } + } + catch (Exception e) + { + Debug.LogError($"Failed to update appxmanifest!\n{e.Message}"); + return IsBuilding = false; + } + + string storagePath = Path.GetFullPath(Path.Combine(Path.Combine(Application.dataPath, ".."), buildInfo.OutputDirectory)); + string solutionProjectPath = Path.GetFullPath(Path.Combine(storagePath, $@"{PlayerSettings.productName}.sln")); + + int exitCode; + + // Building the solution requires first restoring NuGet packages - when built through + // Visual Studio, VS does this automatically - when building via msbuild like we're doing here, + // we have to do that step manually. + // We use msbuild for nuget restore by default, but if a path to nuget.exe is supplied then we use that executable + if (string.IsNullOrEmpty(buildInfo.NugetExecutablePath)) + { + exitCode = await Run(msBuildPath, + $"\"{solutionProjectPath}\" /t:restore {GetMSBuildLoggingCommand(buildInfo.LogDirectory, "nugetRestore.log")}", + !Application.isBatchMode, + cancellationToken); + } + else + { + exitCode = await Run(buildInfo.NugetExecutablePath, + $"restore \"{solutionProjectPath}\"", + !Application.isBatchMode, + cancellationToken); + } + + if (exitCode != 0) + { + IsBuilding = false; + return false; + } + + // Need to add ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch to MixedRealityToolkit.vcxproj + if (buildInfo.BuildPlatform == "arm64") + { + if (!UpdateVSProj(buildInfo)) + { + return IsBuilding = false; + } + } + + // Now that NuGet packages have been restored, we can run the actual build process. + exitCode = await Run(msBuildPath, + $"\"{solutionProjectPath}\" {(buildInfo.Multicore ? "/m /nr:false" : "")} /t:{(buildInfo.RebuildAppx ? "Rebuild" : "Build")} /p:Configuration={buildInfo.Configuration} /p:Platform={buildInfo.BuildPlatform} {(string.IsNullOrEmpty(buildInfo.PlatformToolset) ? string.Empty : $"/p:PlatformToolset={buildInfo.PlatformToolset}")} {GetMSBuildLoggingCommand(buildInfo.LogDirectory, "buildAppx.log")}", + !Application.isBatchMode, + cancellationToken); + AssetDatabase.SaveAssets(); + + IsBuilding = false; + return exitCode == 0; + } + + private static async Task Run(string fileName, string args, bool showDebug, CancellationToken cancellationToken) + { + Debug.Log($"Running command: {fileName} {args}"); + + var processResult = await new Process().StartProcessAsync( + fileName, args, !Application.isBatchMode, cancellationToken); + + switch (processResult.ExitCode) + { + case 0: + Debug.Log($"Command successful"); + + if (Application.isBatchMode) + { + Debug.Log(string.Join("\n", processResult.Output)); + } + break; + case -1073741510: + Debug.LogWarning("The build was terminated either by user's keyboard input CTRL+C or CTRL+Break or closing command prompt window."); + break; + default: + { + if (processResult.ExitCode != 0) + { + Debug.Log($"Command failed, errorCode: {processResult.ExitCode}"); + + if (Application.isBatchMode) + { + var output = "Command output:\n"; + + foreach (var message in processResult.Output) + { + output += $"{message}\n"; + } + + output += "Command errors:"; + + foreach (var error in processResult.Errors) + { + output += $"{error}\n"; + } + + Debug.LogError(output); + } + } + break; + } + } + return processResult.ExitCode; + } + + private static async Task FindMsBuildPathAsync() + { + // Finding msbuild.exe involves different work depending on whether or not users + // have VS2017 or VS2019 installed. + foreach (VSWhereFindOption findOption in VSWhereFindOptions) + { + string arguments = findOption.arguments; + if (string.IsNullOrWhiteSpace(EditorUserBuildSettings.wsaUWPVisualStudioVersion)) + { + arguments += " -latest"; + } + else + { + // Add version number with brackets to find only the specified version + arguments += $" -version [{EditorUserBuildSettings.wsaUWPVisualStudioVersion}]"; + } + + var result = await new Process().StartProcessAsync( + new ProcessStartInfo + { + FileName = "cmd.exe", + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + Arguments = arguments, + WorkingDirectory = @"C:\Program Files (x86)\Microsoft Visual Studio\Installer" + }); + + foreach (var path in result.Output) + { + if (!string.IsNullOrEmpty(path)) + { + string[] paths = path.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + + if (paths.Length > 0) + { + // if there are multiple visual studio installs, + // prefer enterprise, then pro, then community + string bestPath = paths.OrderByDescending(p => p.ToLower().Contains("enterprise")) + .ThenByDescending(p => p.ToLower().Contains("professional")) + .ThenByDescending(p => p.ToLower().Contains("community")).First(); + + string finalPath = $@"{bestPath}{findOption.pathSuffix}"; + if (File.Exists(finalPath)) + { + return finalPath; + } + } + } + } + } + + return string.Empty; + } + + private static bool UpdateVSProj(IBuildInfo buildInfo) + { + // For ARM64 builds we need to add ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch + // to vcxproj file in order to ensure that the build passes + string projectFilePath = GetProjectFilePath(buildInfo); + if (projectFilePath == null) + { + return false; + } + + var rootNode = XElement.Load(projectFilePath); + var defaultNamespace = rootNode.GetDefaultNamespace(); + var propertyGroupNode = rootNode.Element(defaultNamespace + "PropertyGroup"); + + if (propertyGroupNode == null) + { + propertyGroupNode = new XElement(defaultNamespace + "PropertyGroup", new XAttribute("Label", "Globals")); + rootNode.Add(propertyGroupNode); + } + + var newNode = propertyGroupNode.Element(defaultNamespace + "ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch"); + if (newNode != null) + { + // If this setting already exists in the project, ensure its value is "None" + newNode.Value = "None"; + } + else + { + propertyGroupNode.Add(new XElement(defaultNamespace + "ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch", "None")); + } + + rootNode.Save(projectFilePath); + + return true; + } + + /// + /// Given the project name and build path, resolves the valid VcProject file (i.e. .vcsproj, vcxproj) + /// + /// A valid path if the project file exists, null otherwise + private static string GetProjectFilePath(IBuildInfo buildInfo) + { + string projectName = PlayerSettings.productName; + foreach (string extension in VcProjExtensions) + { + string projectFilePath = Path.Combine(Path.GetFullPath(buildInfo.OutputDirectory), projectName, $"{projectName}.{extension}"); + if (File.Exists(projectFilePath)) + { + return projectFilePath; + } + } + + string projectDirectory = Path.Combine(Path.GetFullPath(buildInfo.OutputDirectory), projectName); + string combinedExtensions = String.Join("|", VcProjExtensions); + Debug.LogError($"Cannot find project file {projectDirectory} given names {projectName}.{combinedExtensions}"); + return null; + } + + private static bool UpdateAppxManifest(IBuildInfo buildInfo) + { + string manifestFilePath = GetManifestFilePath(buildInfo); + if (manifestFilePath == null) + { + // Error has already been logged + return false; + } + + var rootNode = XElement.Load(manifestFilePath); + var identityNode = rootNode.Element(rootNode.GetDefaultNamespace() + "Identity"); + + if (identityNode == null) + { + Debug.LogError($"Package.appxmanifest for build (in path - {manifestFilePath}) is missing an node"); + return false; + } + + var dependencies = rootNode.Element(rootNode.GetDefaultNamespace() + "Dependencies"); + + if (dependencies == null) + { + Debug.LogError($"Package.appxmanifest for build (in path - {manifestFilePath}) is missing node."); + return false; + } + + UpdateDependenciesElement(dependencies, rootNode.GetDefaultNamespace()); + AddCapabilities(buildInfo, rootNode); + + // We use XName.Get instead of string -> XName implicit conversion because + // when we pass in the string "Version", the program doesn't find the attribute. + // Best guess as to why this happens is that implicit string conversion doesn't set the namespace to empty + var versionAttr = identityNode.Attribute(XName.Get("Version")); + + if (versionAttr == null) + { + Debug.LogError($"Package.appxmanifest for build (in path - {manifestFilePath}) is missing a Version attribute in the node."); + return false; + } + + // Assume package version always has a '.' between each number. + // According to https://msdn.microsoft.com/library/windows/apps/br211441.aspx + // Package versions are always of the form Major.Minor.Build.Revision. + // Note: Revision number reserved for Windows Store, and a value other than 0 will fail WACK. + var version = PlayerSettings.WSA.packageVersion; + var newVersion = new Version(version.Major, version.Minor, buildInfo.AutoIncrement ? version.Build + 1 : version.Build, version.Revision); + + PlayerSettings.WSA.packageVersion = newVersion; + versionAttr.Value = newVersion.ToString(); + rootNode.Save(manifestFilePath); + return true; + } + + /// + /// Gets the AppX manifest path in the project output directory. + /// + private static string GetManifestFilePath(IBuildInfo buildInfo) + { + var fullPathOutputDirectory = Path.GetFullPath(buildInfo.OutputDirectory); + Debug.Log($"Searching for appx manifest in {fullPathOutputDirectory}..."); + + // Find the manifest, assume the one we want is the first one + string[] manifests = Directory.GetFiles(fullPathOutputDirectory, "Package.appxmanifest", SearchOption.AllDirectories); + + if (manifests.Length == 0) + { + Debug.LogError($"Unable to find Package.appxmanifest file for build (in path - {fullPathOutputDirectory})"); + return null; + } + + if (manifests.Length > 1) + { + Debug.LogWarning("Found more than one appxmanifest in the target build folder!"); + } + + return manifests[0]; + } + + /// + /// Updates 'Assembly-CSharp.csproj' file according to the values set in buildInfo. + /// + /// An IBuildInfo containing a valid OutputDirectory + /// Only used with the .NET backend in Unity 2018 or older, with Unity C# Projects enabled. + public static void UpdateAssemblyCSharpProject(IBuildInfo buildInfo) + { +#if !UNITY_2019_1_OR_NEWER + if (!EditorUserBuildSettings.wsaGenerateReferenceProjects || + PlayerSettings.GetScriptingBackend(BuildTargetGroup.WSA) != ScriptingImplementation.WinRTDotNET) + { + // Assembly-CSharp.csproj is only generated when the above is true + return; + } + + string projectFilePath = GetAssemblyCSharpProjectFilePath(buildInfo); + if (projectFilePath == null) + { + throw new FileNotFoundException("Unable to find 'Assembly-CSharp.csproj' file."); + } + + var rootElement = XElement.Load(projectFilePath); + var uwpBuildInfo = buildInfo as UwpBuildInfo; + Debug.Assert(uwpBuildInfo != null); + + if (uwpBuildInfo.AllowUnsafeCode) + { + AllowUnsafeCode(rootElement); + } + + rootElement.Save(projectFilePath); +#endif // !UNITY_2019_1_OR_NEWER + } + + /// + /// Gets the 'Assembly-CSharp.csproj' files path in the project output directory. + /// + private static string GetAssemblyCSharpProjectFilePath(IBuildInfo buildInfo) + { + var fullPathOutputDirectory = Path.GetFullPath(buildInfo.OutputDirectory); + Debug.Log($"Searching for 'Assembly-CSharp.csproj' in {fullPathOutputDirectory}..."); + + // Find the manifest, assume the one we want is the first one + string[] manifests = Directory.GetFiles(fullPathOutputDirectory, "Assembly-CSharp.csproj", SearchOption.AllDirectories); + + if (manifests.Length == 0) + { + Debug.LogError($"Unable to find 'Assembly-CSharp.csproj' file for build (in path - {fullPathOutputDirectory})"); + return null; + } + + if (manifests.Length > 1) + { + Debug.LogWarning("Found more than one 'Assembly-CSharp.csproj' in the target build folder!"); + } + + return manifests[0]; + } + + private static void UpdateDependenciesElement(XElement dependencies, XNamespace defaultNamespace) + { + var values = (PlayerSettings.WSATargetFamily[])Enum.GetValues(typeof(PlayerSettings.WSATargetFamily)); + + if (string.IsNullOrWhiteSpace(EditorUserBuildSettings.wsaUWPSDK)) + { + var windowsSdkPaths = Directory.GetDirectories(@"C:\Program Files (x86)\Windows Kits\10\Lib"); + + for (int i = 0; i < windowsSdkPaths.Length; i++) + { + windowsSdkPaths[i] = windowsSdkPaths[i].Substring(windowsSdkPaths[i].LastIndexOf(@"\", StringComparison.Ordinal) + 1); + } + + EditorUserBuildSettings.wsaUWPSDK = windowsSdkPaths[windowsSdkPaths.Length - 1]; + } + + string maxVersionTested = EditorUserBuildSettings.wsaUWPSDK; + + if (string.IsNullOrWhiteSpace(EditorUserBuildSettings.wsaMinUWPSDK)) + { + EditorUserBuildSettings.wsaMinUWPSDK = UwpBuildDeployPreferences.MIN_PLATFORM_VERSION.ToString(); + } + + string minVersion = EditorUserBuildSettings.wsaMinUWPSDK; + + // Clear any we had before. + dependencies.RemoveAll(); + + foreach (PlayerSettings.WSATargetFamily family in values) + { + if (PlayerSettings.WSA.GetTargetDeviceFamily(family)) + { + dependencies.Add( + new XElement(defaultNamespace + "TargetDeviceFamily", + new XAttribute("Name", $"Windows.{family}"), + new XAttribute("MinVersion", minVersion), + new XAttribute("MaxVersionTested", maxVersionTested))); + } + } + + if (!dependencies.HasElements) + { + dependencies.Add( + new XElement(defaultNamespace + "TargetDeviceFamily", + new XAttribute("Name", "Windows.Universal"), + new XAttribute("MinVersion", minVersion), + new XAttribute("MaxVersionTested", maxVersionTested))); + } + } + + /// Gets the subpart of the msbuild.exe command to save log information + /// in the given logFileName. + /// + /// + /// Will return an empty string if logDirectory is not set. + /// + private static string GetMSBuildLoggingCommand(string logDirectory, string logFileName) + { + if (String.IsNullOrEmpty(logDirectory)) + { + Debug.Log($"Not logging {logFileName} because no logDirectory was provided"); + return ""; + } + + return $"-fl -flp:logfile={Path.Combine(logDirectory, logFileName)};verbosity=detailed"; + } + + /// + /// Adds capabilities according to the values in the buildInfo to the manifest file. + /// + /// An IBuildInfo containing a valid OutputDirectory and all capabilities + public static void AddCapabilities(IBuildInfo buildInfo, XElement rootElement = null) + { + var manifestFilePath = GetManifestFilePath(buildInfo); + if (manifestFilePath == null) + { + throw new FileNotFoundException("Unable to find manifest file"); + } + + rootElement = rootElement ?? XElement.Load(manifestFilePath); + var uwpBuildInfo = buildInfo as UwpBuildInfo; + + Debug.Assert(uwpBuildInfo != null); + + // Here, ResearchModeCapability must come first, in order to avoid schema errors + // See https://docs.microsoft.com/windows/uwp/packaging/app-capability-declarations#restricted-capabilities + if (uwpBuildInfo.ResearchModeCapabilityEnabled +#if !UNITY_2021_2_OR_NEWER + && EditorUserBuildSettings.wsaSubtarget == WSASubtarget.HoloLens +#endif // !UNITY_2021_2_OR_NEWER + ) + { + AddResearchModeCapability(rootElement); + } + + if (uwpBuildInfo.DeviceCapabilities != null) + { + AddCapabilities(rootElement, uwpBuildInfo.DeviceCapabilities); + } + if (uwpBuildInfo.GazeInputCapabilityEnabled) + { + AddGazeInputCapability(rootElement); + } + + rootElement.Save(manifestFilePath); + } + + /// + /// Adds a capability to the given rootNode, which must be the read AppX manifest from + /// the build output. + /// + /// An XElement containing the AppX manifest from + /// the build output + /// The added capabilities tag as XName + /// Value of the Name-XAttribute of the added capability + public static void AddCapability(XElement rootNode, XName capability, string value) + { + // If the capabilities container tag is missing, make sure it gets added. + var capabilitiesTag = rootNode.GetDefaultNamespace() + "Capabilities"; + XElement capabilitiesNode = rootNode.Element(capabilitiesTag); + if (capabilitiesNode == null) + { + capabilitiesNode = new XElement(capabilitiesTag); + rootNode.Add(capabilitiesNode); + } + + XElement existingCapability = capabilitiesNode.Elements(capability) + .FirstOrDefault(element => element.Attribute("Name")?.Value == value); + + // Only add the capability if it isn't there already. + if (existingCapability == null) + { + capabilitiesNode.Add( + new XElement(capability, new XAttribute("Name", value))); + } + } + + /// + /// Adds the 'Gaze Input' capability to the manifest. + /// + /// + /// This is a workaround for versions of Unity which don't have native support + /// for the 'Gaze Input' capability in its Player Settings preference location. + /// Note that this function is only public to poke a hole for testing - do not + /// take a dependency on this function. + /// + public static void AddGazeInputCapability(XElement rootNode) + { + AddCapability(rootNode, rootNode.GetDefaultNamespace() + "DeviceCapability", "gazeInput"); + } + + /// + /// Adds the given capabilities to the manifest. + /// + public static void AddCapabilities(XElement rootNode, List capabilities) + { + foreach (string capability in capabilities) + { + AddCapability(rootNode, rootNode.GetDefaultNamespace() + "DeviceCapability", capability); + } + } + + /// + /// Adds the 'Research Mode' capability to the manifest. + /// + /// + /// This is only for research projects and should not be used in production. + /// For further information take a look at https://docs.microsoft.com/windows/mixed-reality/research-mode. + /// Note that this function is only public to poke a hole for testing - do not + /// take a dependency on this function. + /// + public static void AddResearchModeCapability(XElement rootNode) + { + // Add rescap Namespace to package tag + XNamespace rescapNs = "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"; + var rescapAttribute = rootNode.Attribute(XNamespace.Xmlns + "rescap"); + if (rescapAttribute == null) + { + rescapAttribute = new XAttribute(XNamespace.Xmlns + "rescap", rescapNs); + rootNode.Add(rescapAttribute); + } + + // Add rescap to IgnorableNamespaces + var ignNsAttribute = rootNode.Attribute("IgnorableNamespaces"); + if (ignNsAttribute == null) + { + ignNsAttribute = new XAttribute("IgnorableNamespaces", "rescap"); + rootNode.Add(ignNsAttribute); + } + + if (!ignNsAttribute.Value.Contains("rescap")) + { + ignNsAttribute.Value += " rescap"; + } + + AddCapability(rootNode, rescapNs + "Capability", "perceptionSensorsExperimental"); + } + + /// + /// Enables unsafe code in the generated Assembly-CSharp project. + /// + /// + /// This is not required by the research mode, but not using unsafe code with + /// direct memory access results in poor performance. So it is recommended + /// to use unsafe code to an extent. + /// For further information take a look at https://docs.microsoft.com/windows/mixed-reality/research-mode. + /// Note that this function is only public to poke a hole for testing - do not + /// take a dependency on this function. + /// + public static void AllowUnsafeCode(XElement rootNode) + { + foreach (XElement propertyGroupNode in rootNode.Descendants(rootNode.GetDefaultNamespace() + "PropertyGroup")) + { + if (propertyGroupNode.Attribute("Condition") != null) + { + var allowUnsafeBlocks = propertyGroupNode.Element(propertyGroupNode.GetDefaultNamespace() + "AllowUnsafeBlocks"); + if (allowUnsafeBlocks == null) + { + allowUnsafeBlocks = new XElement(propertyGroupNode.GetDefaultNamespace() + "AllowUnsafeBlocks"); + propertyGroupNode.Add(allowUnsafeBlocks); + } + allowUnsafeBlocks.Value = "true"; + } + } + } + + /// + /// This struct controls the behavior of the arguments that are used + /// when finding msbuild.exe. + /// + private struct VSWhereFindOption + { + public VSWhereFindOption(string args, string suffix) + { + arguments = args; + pathSuffix = suffix; + } + + /// + /// Used to populate the Arguments of ProcessStartInfo when invoking + /// vswhere. + /// + public string arguments; + + /// + /// This string is added as a suffix to the result of the vswhere path + /// search. + /// + public string pathSuffix; + } + + private static readonly VSWhereFindOption[] VSWhereFindOptions = + { + // This find option corresponds to the version of vswhere that ships with VS2019. + new VSWhereFindOption( + @"/C vswhere -all -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe", + ""), + // This find option corresponds to the version of vswhere that ships with VS2017 - this doesn't have + // support for the -find command switch. + new VSWhereFindOption( + @"/C vswhere -all -products * -requires Microsoft.Component.MSBuild -property installationPath", + "\\MSBuild\\15.0\\Bin\\MSBuild.exe"), + }; + } +} diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpAppxBuildTools.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpAppxBuildTools.cs.meta new file mode 100644 index 00000000000..a995e769ae3 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpAppxBuildTools.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 14ddd58f496c4152b9b8c8c995d89443 +timeCreated: 1663169360 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildDeployPreferences.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildDeployPreferences.cs new file mode 100644 index 00000000000..cbddf8d9d94 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildDeployPreferences.cs @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.MixedReality.Toolkit.Utilities.Editor; +using Microsoft.MixedReality.Toolkit.WindowsDevicePortal; +using System; +using UnityEditor; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.Build.Editor +{ + public static class UwpBuildDeployPreferences + { + /// + /// The minimum Windows SDK that must be present on the build machine in order + /// for a build to be successful. + /// + /// + /// This controls the version of the Windows SDK that is build against on the local + /// machine, NOT the version of the OS that must be present on the device that + /// the built application is deployed to (this other aspect is controlled by + /// MIN_PLATFORM_VERSION) + /// + public static Version MIN_SDK_VERSION = new Version("10.0.18362.0"); + + /// + /// The minimum version of the OS that must exist on the device that the application + /// is deployed to. + /// + /// + /// This is intentionally set to a very low version, so that the application can be + /// deployed to variety of different devices which may be on older OS versions. + /// + public static Version MIN_PLATFORM_VERSION = new Version("10.0.10240.0"); + + private const string EDITOR_PREF_BUILD_CONFIG = "BuildDeployWindow_BuildConfig"; + private const string EDITOR_PREF_PLATFORM_TOOLSET = "BuildDeployWindow_PlatformToolset"; + private const string EDITOR_PREF_FORCE_REBUILD = "BuildDeployWindow_ForceRebuild"; + private const string EDITOR_PREF_CONNECT_INFOS = "BuildDeployWindow_DeviceConnections"; + private const string EDITOR_PREF_LOCAL_CONNECT_INFO = "BuildDeployWindow_LocalConnection"; + private const string EDITOR_PREF_FULL_REINSTALL = "BuildDeployWindow_FullReinstall"; + private const string EDITOR_PREF_USE_SSL = "BuildDeployWindow_UseSSL"; + private const string EDITOR_PREF_VERIFY_SSL = "BuildDeployWindow_VerifySSL"; + private const string EDITOR_PREF_PROCESS_ALL = "BuildDeployWindow_ProcessAll"; + private const string EDITOR_PREF_GAZE_INPUT_CAPABILITY_ENABLED = "BuildDeployWindow_GazeInputCapabilityEnabled"; + private const string EDITOR_PREF_MULTICORE_APPX_BUILD_ENABLED = "BuildDeployWindow_MulticoreAppxBuildEnabled"; + private const string EDITOR_PREF_RESEARCH_MODE_CAPABILITY_ENABLED = "BuildDeployWindow_ResearchModeCapabilityEnabled"; + private const string EDITOR_PREF_ALLOW_UNSAFE_CODE = "BuildDeployWindow_AllowUnsafeCode"; + private const string EDITOR_PREF_NUGET_EXECUTABLE_PATH = "BuildDeployWindow_NugetExecutablePath"; + + /// + /// The current Build Configuration. (Debug, Release, or Master) + /// + public static string BuildConfig + { + get => EditorPreferences.Get(EDITOR_PREF_BUILD_CONFIG, "master"); + set => EditorPreferences.Set(EDITOR_PREF_BUILD_CONFIG, value.ToLower()); + } + + /// + /// Gets the build configuration type as a WSABuildType enum + /// + public static WSABuildType BuildConfigType + { + get + { + string curBuildConfigString = BuildConfig; + if (curBuildConfigString.Equals("master", StringComparison.OrdinalIgnoreCase)) + { + return WSABuildType.Master; + } + else if (curBuildConfigString.Equals("release", StringComparison.OrdinalIgnoreCase)) + { + return WSABuildType.Release; + } + else + { + return WSABuildType.Debug; + } + } + } + + /// + /// The current Platform Toolset. (Solution, v141, or v142) + /// + public static string PlatformToolset + { + get => EditorPreferences.Get(EDITOR_PREF_PLATFORM_TOOLSET, string.Empty); + set => EditorPreferences.Set(EDITOR_PREF_PLATFORM_TOOLSET, value.ToLower()); + } + + /// + /// Current setting to force rebuilding the appx. + /// + public static bool ForceRebuild + { + get => EditorPreferences.Get(EDITOR_PREF_FORCE_REBUILD, false); + set => EditorPreferences.Set(EDITOR_PREF_FORCE_REBUILD, value); + } + + /// + /// Current setting to fully uninstall and reinstall the appx. + /// + public static bool FullReinstall + { + get => EditorPreferences.Get(EDITOR_PREF_FULL_REINSTALL, true); + set => EditorPreferences.Set(EDITOR_PREF_FULL_REINSTALL, value); + } + + /// + /// The current device portal connections. + /// + public static string DevicePortalConnections + { + get => EditorPreferences.Get( + EDITOR_PREF_CONNECT_INFOS, + JsonUtility.ToJson( + new DevicePortalConnections( + new DeviceInfo(DeviceInfo.LocalIPAddress, string.Empty, string.Empty, DeviceInfo.LocalMachine)))); + set => EditorPreferences.Set(EDITOR_PREF_CONNECT_INFOS, value); + } + + /// + /// The current device portal connections. + /// + public static string LocalConnectionInfo + { + get => EditorPreferences.Get( + EDITOR_PREF_LOCAL_CONNECT_INFO, + JsonUtility.ToJson(new DeviceInfo(DeviceInfo.LocalIPAddress, string.Empty, string.Empty, DeviceInfo.LocalMachine))); + set => EditorPreferences.Set(EDITOR_PREF_LOCAL_CONNECT_INFO, value); + } + + /// + /// Current setting to use Single Socket Layer connections to the device portal. + /// + public static bool UseSSL + { + get => EditorPreferences.Get(EDITOR_PREF_USE_SSL, false); + set => EditorPreferences.Set(EDITOR_PREF_USE_SSL, value); + } + + /// + /// Current setting to verify SSL certificates for connections to the device portal. + /// + public static bool VerifySSL + { + get => EditorPreferences.Get(EDITOR_PREF_VERIFY_SSL, true); + set => EditorPreferences.Set(EDITOR_PREF_VERIFY_SSL, value); + } + + /// + /// Current setting to target all the devices registered to the build window. + /// + public static bool TargetAllConnections + { + get => EditorPreferences.Get(EDITOR_PREF_PROCESS_ALL, false); + set => EditorPreferences.Set(EDITOR_PREF_PROCESS_ALL, value); + } + + /// + /// If true, the 'Gaze Input' capability will be added to the AppX manifest + /// after the Unity build. + /// + public static bool GazeInputCapabilityEnabled + { + get => EditorPreferences.Get(EDITOR_PREF_GAZE_INPUT_CAPABILITY_ENABLED, false); + set => EditorPreferences.Set(EDITOR_PREF_GAZE_INPUT_CAPABILITY_ENABLED, value); + } + + /// + /// If true, the appx will be built with multicore support enabled in the + /// MSBuild process. + /// + public static bool MulticoreAppxBuildEnabled + { + get => EditorPreferences.Get(EDITOR_PREF_MULTICORE_APPX_BUILD_ENABLED, false); + set => EditorPreferences.Set(EDITOR_PREF_MULTICORE_APPX_BUILD_ENABLED, value); + } + + /// + /// Current setting to modify 'Package.appxmanifest' file for sensor access. + /// + public static bool ResearchModeCapabilityEnabled + { + get => EditorPreferences.Get(EDITOR_PREF_RESEARCH_MODE_CAPABILITY_ENABLED, false); + set => EditorPreferences.Set(EDITOR_PREF_RESEARCH_MODE_CAPABILITY_ENABLED, value); + } + + /// + /// Current setting to modify 'Assembly-CSharp.csproj' file to allow unsafe code. + /// + public static bool AllowUnsafeCode + { + get => EditorPreferences.Get(EDITOR_PREF_ALLOW_UNSAFE_CODE, false); + set => EditorPreferences.Set(EDITOR_PREF_ALLOW_UNSAFE_CODE, value); + } + + /// + /// Current value of the optional path to nuget.exe. + /// + public static string NugetExecutablePath + { + get => EditorPreferences.Get(EDITOR_PREF_NUGET_EXECUTABLE_PATH, string.Empty); + set => EditorPreferences.Set(EDITOR_PREF_NUGET_EXECUTABLE_PATH, value); + } + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildDeployPreferences.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildDeployPreferences.cs.meta new file mode 100644 index 00000000000..96b7d1d1aa2 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildDeployPreferences.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8bcc29bf358d482d9d21279964cd1ebf +timeCreated: 1663169360 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildInfo.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildInfo.cs new file mode 100644 index 00000000000..065f56998cb --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildInfo.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using UnityEditor; + +namespace Microsoft.MixedReality.Toolkit.Build.Editor +{ + public class UwpBuildInfo : BuildInfo + { + public UwpBuildInfo(bool isCommandLine = false) : base(isCommandLine) + { + } + + /// + public override BuildTarget BuildTarget => BuildTarget.WSAPlayer; + + /// + /// Build the appx bundle after building Unity Player? + /// + public bool BuildAppx { get; set; } = false; + + /// + /// Force rebuilding the appx bundle? + /// + public bool RebuildAppx { get; set; } = false; + + /// + /// VC Platform Toolset used building the appx bundle + /// + public string PlatformToolset { get; set; } + + /// + /// If true, the 'Gaze Input' capability will be added to the AppX + /// manifest after the Unity build. + /// + public bool GazeInputCapabilityEnabled { get; set; } = false; + + /// + /// Use multiple cores for building the appx bundle? + /// + public bool Multicore { get; set; } = false; + + /// + /// If true, the 'Research Mode' capability will be added to the AppX + /// manifest after the Unity build. + /// + public bool ResearchModeCapabilityEnabled { get; set; } = false; + + /// + /// If true, unsafe code will be allowed in the generated + /// Assembly-CSharp project. + /// + public bool AllowUnsafeCode { get; set; } = false; + + /// + /// When present, adds a DeviceCapability for each entry + /// in the list to the manifest + /// + public List DeviceCapabilities { get; set; } = null; + + /// + /// Optional path to nuget.exe. Used when performing package restore with nuget.exe (instead of msbuild) is desired. + /// + public string NugetExecutablePath { get; set; } + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildInfo.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildInfo.cs.meta new file mode 100644 index 00000000000..7cf130a70f2 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpBuildInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d2271f24e63049e69370386ffca360c7 +timeCreated: 1663169360 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpPlayerBuildTools.cs b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpPlayerBuildTools.cs new file mode 100644 index 00000000000..fc255875ce7 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpPlayerBuildTools.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.MixedReality.Toolkit.Utilities.Editor; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEditor.Build.Reporting; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.Build.Editor +{ + /// + /// Class containing various utility methods to build a WSA solution from a Unity project. + /// + public static class UwpPlayerBuildTools + { + private static void ParseBuildCommandLine(ref UwpBuildInfo buildInfo) + { + IBuildInfo iBuildInfo = buildInfo; + UnityPlayerBuildTools.ParseBuildCommandLine(ref iBuildInfo); + + string[] arguments = Environment.GetCommandLineArgs(); + + for (int i = 0; i < arguments.Length; ++i) + { + switch (arguments[i]) + { + case "-buildAppx": + buildInfo.BuildAppx = true; + break; + case "-rebuildAppx": + buildInfo.RebuildAppx = true; + break; + case "-targetUwpSdk": + // Note: the min sdk target cannot be changed. + EditorUserBuildSettings.wsaUWPSDK = arguments[++i]; + break; + case "-nugetPath": + buildInfo.NugetExecutablePath = arguments[++i]; + break; + } + } + } + + /// + /// Do a build configured for UWP Applications to the specified path, returns the error from + /// + /// Should the user be prompted to build the appx as well? + /// True, if build was successful. + public static async Task BuildPlayer(string buildDirectory, bool showDialog = true, CancellationToken cancellationToken = default) + { + if (UnityPlayerBuildTools.CheckBuildScenes() == false) + { + return false; + } + + var buildInfo = new UwpBuildInfo + { + OutputDirectory = buildDirectory, + Scenes = EditorBuildSettings.scenes.Where(scene => scene.enabled && !string.IsNullOrEmpty(scene.path)).Select(scene => scene.path), + BuildAppx = !showDialog, + GazeInputCapabilityEnabled = UwpBuildDeployPreferences.GazeInputCapabilityEnabled, + + // Configure Appx build preferences for post build action + RebuildAppx = UwpBuildDeployPreferences.ForceRebuild, + Configuration = UwpBuildDeployPreferences.BuildConfig, + BuildPlatform = EditorUserBuildSettings.wsaArchitecture, + PlatformToolset = UwpBuildDeployPreferences.PlatformToolset, + AutoIncrement = BuildDeployPreferences.IncrementBuildVersion, + Multicore = UwpBuildDeployPreferences.MulticoreAppxBuildEnabled, + ResearchModeCapabilityEnabled = UwpBuildDeployPreferences.ResearchModeCapabilityEnabled, + AllowUnsafeCode = UwpBuildDeployPreferences.AllowUnsafeCode, + + // Configure a post build action that will compile the generated solution + PostBuildAction = PostBuildAction + }; + + async void PostBuildAction(IBuildInfo innerBuildInfo, BuildReport buildReport) + { + if (buildReport.summary.result != BuildResult.Succeeded) + { + EditorUtility.DisplayDialog($"{PlayerSettings.productName} WindowsStoreApp Build {buildReport.summary.result}!", "See console for details", "OK"); + } + else + { + var uwpBuildInfo = innerBuildInfo as UwpBuildInfo; + Debug.Assert(uwpBuildInfo != null); + UwpAppxBuildTools.AddCapabilities(uwpBuildInfo); + UwpAppxBuildTools.UpdateAssemblyCSharpProject(uwpBuildInfo); + + if (showDialog && + !EditorUtility.DisplayDialog(PlayerSettings.productName, "Build Complete", "OK", "Build AppX")) + { + EditorAssemblyReloadManager.LockReloadAssemblies = true; + await UwpAppxBuildTools.BuildAppxAsync(uwpBuildInfo, cancellationToken); + EditorAssemblyReloadManager.LockReloadAssemblies = false; + } + } + } + + return await BuildPlayer(buildInfo, cancellationToken); + } + + /// + /// Build the UWP Player. + /// + public static async Task BuildPlayer(UwpBuildInfo buildInfo, CancellationToken cancellationToken = default) + { + #region Gather Build Data + + if (buildInfo.IsCommandLine) + { + ParseBuildCommandLine(ref buildInfo); + } + + #endregion Gather Build Data + + BuildReport buildReport = UnityPlayerBuildTools.BuildUnityPlayer(buildInfo); + + bool success = buildReport != null && buildReport.summary.result == BuildResult.Succeeded; + + if (success && buildInfo.BuildAppx) + { + success &= await UwpAppxBuildTools.BuildAppxAsync(buildInfo, cancellationToken); + } + + return success; + } + } +} diff --git a/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpPlayerBuildTools.cs.meta b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpPlayerBuildTools.cs.meta new file mode 100644 index 00000000000..e4355a64e18 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildAndDeploy/UwpPlayerBuildTools.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b8c458559c834d3dba6ae6320b0ab8e5 +timeCreated: 1663169360 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/BuildDeployWindow.cs b/com.microsoft.mrtk.buildwindow/BuildDeployWindow.cs new file mode 100644 index 00000000000..89a91108a76 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildDeployWindow.cs @@ -0,0 +1,1697 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.MixedReality.Toolkit.Utilities.Editor; +using Microsoft.MixedReality.Toolkit.WindowsDevicePortal; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using UnityEditor; +using UnityEngine; +using Debug = UnityEngine.Debug; +using FileInfo = System.IO.FileInfo; + +namespace Microsoft.MixedReality.Toolkit.Build.Editor +{ + /// + /// Build window - supports SLN creation, APPX from SLN, Deploy on device, and misc helper utilities associated with the build/deploy/test iteration loop + /// Requires the device to be set in developer mode and to have secure connections disabled (in the security tab in the device portal) + /// + public class BuildDeployWindow : EditorWindow + { + #region Internal Types + + private enum BuildDeployTab + { + UnityBuildOptions, + AppxBuildOptions, + DeployOptions + } + +#if UNITY_2021_2_OR_NEWER + /// + /// Matches the deprecated WSASubtarget. + /// + private enum UWPSubtarget + { + AnyDevice = 0, + PC = 1, + Mobile = 2, + HoloLens = 3 + } +#endif // UNITY_2021_2_OR_NEWER + + #endregion Internal Types + + #region Constants and Readonly Values + + private const string UseRemoteTargetSessionKey = "DeployWindow_UseRemoteTarget"; + + private const string HOLOLENS_USB = "HoloLens over USB"; + + private const string EMPTY_IP_ADDRESS = "0.0.0.0"; + + private const string WifiAdapterType = "IEEE 802"; + + private static readonly string[] TAB_NAMES = { "Unity Build Options", "Appx Build Options", "Deploy Options" }; + + private static readonly string[] SCRIPTING_BACKEND_NAMES = { "IL2CPP", ".NET" }; + + private static readonly int[] SCRIPTING_BACKEND_ENUMS = { (int)ScriptingImplementation.IL2CPP, (int)ScriptingImplementation.WinRTDotNET }; + + private static readonly string[] TARGET_DEVICE_OPTIONS = { "Any Device", "PC", "Mobile", "HoloLens" }; + + private static readonly string[] ARCHITECTURE_OPTIONS = { + "x86", + "x64", + "ARM", + #if UNITY_2019_1_OR_NEWER + "ARM64" + #endif // UNITY_2019_1_OR_NEWER + }; + + private static readonly string[] PLATFORM_TOOLSET_VALUES = { string.Empty, "v141", "v142" }; + + private static readonly string[] PLATFORM_TOOLSET_NAMES = { "Solution", "v141", "v142" }; + + private static readonly string[] LocalRemoteOptions = { "Local", "Remote" }; + + private static readonly List Builds = new List(0); + + private static readonly List AppPackageDirectories = new List(0); + + private const string BuildWindowTabKey = "_BuildWindow_Tab"; + + private const string WINDOWS_10_KITS_PATH_REGISTRY_PATH = @"SOFTWARE\Microsoft\Windows Kits\Installed Roots"; + + private const string WINDOWS_10_KITS_PATH_ALTERNATE_REGISTRY_PATH = @"SOFTWARE\WOW6432Node\Microsoft\Windows Kits\Installed Roots"; + + private const string WINDOWS_10_KITS_PATH_REGISTRY_KEY = "KitsRoot10"; + + private const string WINDOWS_10_KITS_PATH_POSTFIX = "Lib"; + + private const string WINDOWS_10_KITS_DEFAULT_PATH = @"C:\Program Files (x86)\Windows Kits\10\Lib"; + + // StandardAssets/Textures/MRTK_Logo_Black.png + private const string LogoLightThemeGuid = "fa0038d8d2df1dd4c99f346c8ec9e746"; + // StandardAssets/Textures/MRTK_Logo_White.png + private const string LogoDarkThemeGuid = "fe5cc215f12ea5e40b5021c4040bce24"; + + private static Texture2D LogoLightTheme; + + private static Texture2D LogoDarkTheme; + + #endregion Constants and Readonly Values + + #region Labels + + private readonly GUIContent BuildAllThenInstallLabel = new GUIContent("Build all, then Install", "Builds the Unity Project, the APPX, then installs to the target device."); + + private readonly GUIContent BuildAllLabel = new GUIContent("Build all", "Builds the Unity Project and APPX"); + + private readonly GUIContent BuildDirectoryLabel = new GUIContent("Build Directory", "It's recommended to use 'UWP'"); + + private readonly GUIContent UseCSharpProjectsLabel = new GUIContent("Generate C# Debug", "Generate C# Project References for debugging.\nOnly available in .NET Scripting runtime."); + + private readonly GUIContent GazeInputCapabilityLabel = + new GUIContent("Gaze Input Capability", + "If checked, the 'Gaze Input' capability will be added to the AppX manifest after the Unity build."); + + private readonly GUIContent AutoIncrementLabel = new GUIContent("Auto Increment", "Increases Version Build Number"); + + private readonly GUIContent VersionNumberLabel = new GUIContent("Version Number", "Major.Minor.Build.Revision\nNote: Revision should always be zero because it's reserved by Windows Store."); + + private readonly GUIContent UseSSLLabel = new GUIContent("Use SSL?", "Use SSL to communicate with Device Portal"); + + private readonly GUIContent VerifySSLLabel = new GUIContent("Verify SSL Certificates?", "When using SSL for Device Portal communication, verify the SSL certificate against Root Certificates. For self-signed Device Portal certificates disabling this omits SSL rejection errors."); + + private readonly GUIContent TargetTypeLabel = new GUIContent("Target Type", "Target either local connection or a remote device"); + + private readonly GUIContent AddConnectionLabel = new GUIContent("+", "Add a remote connection"); + + private readonly GUIContent RemoveConnectionLabel = new GUIContent("-", "Remove a remote connection"); + + private readonly GUIContent IPAddressLabel = new GUIContent("IpAddress", "IP Address for this connection to target"); + + private readonly GUIContent UsernameLabel = new GUIContent("Username", "Device Portal Username to supply for connections and actions"); + + private readonly GUIContent PasswordLabel = new GUIContent("Password", "Device Portal Password to supply for connections and actions"); + + private readonly GUIContent ExecuteOnAllDevicesLabel = new GUIContent("Execute action on all devices", "Should the build options perform actions on all the connected devices?"); + + private readonly GUIContent AlwaysUninstallLabel = new GUIContent("Always uninstall before install", "Uninstall application before installing"); + + private readonly GUIContent ResearchModeCapabilityLabel = new GUIContent("Enable Research Mode", "Enables research mode of HoloLens. This allows access to raw sensor data."); + + private readonly GUIContent AllowUnsafeCodeLabel = new GUIContent("Allow Unsafe Code", "Modify 'Assembly-CSharp.csproj' to allow use of unsafe code. Be careful using this in production."); + + private readonly GUIContent RefreshBuildsLabel = new GUIContent("Refresh Builds", "Re-scan build directory for Appx Packages that can be deployed."); + + private readonly GUIContent InstallAppXLabel = new GUIContent("Install AppX", "Install listed AppX item to either currently selected device or all devices."); + + private readonly GUIContent UninstallAppXLabel = new GUIContent("Uninstall AppX", "Uninstall listed AppX item to either currently selected device or all devices."); + + private readonly GUIContent KillAppLabel = new GUIContent("Kill App", "Kill listed app on either currently selected device or all devices."); + + private readonly GUIContent LaunchAppLabel = new GUIContent("Launch App", "Launch listed app on either currently selected device or all devices."); + + private readonly GUIContent ViewPlayerLogLabel = new GUIContent("View Player Log", "Launch notepad with more recent player log for listed AppX on either currently selected device or from all devices."); + + private readonly GUIContent NugetPathLabel = new GUIContent("Nuget Executable Path", "Only set this when restoring packages with nuget.exe (instead of msbuild) is desired."); + + #endregion Labels + + #region Properties + + private static bool IsValidSdkInstalled { get; set; } = true; + + private static bool ShouldOpenSLNBeEnabled => !string.IsNullOrEmpty(BuildDeployPreferences.BuildDirectory); + + private static bool ShouldBuildSLNBeEnabled => !isBuilding && + !UwpAppxBuildTools.IsBuilding && + !BuildPipeline.isBuildingPlayer && + !string.IsNullOrEmpty(BuildDeployPreferences.BuildDirectory); + + private static bool ShouldBuildAppxBeEnabled => ShouldBuildSLNBeEnabled && !string.IsNullOrEmpty(BuildDeployPreferences.BuildDirectory); + + private static bool DevicePortalConnectionEnabled => (portalConnections.Connections.Count > 1 || IsHoloLensConnectedUsb) && + !string.IsNullOrEmpty(BuildDeployPreferences.BuildDirectory); + + private static bool CanInstall + { + get + { + bool canInstall = true; + if ( +#if UNITY_2021_2_OR_NEWER + currentSubtarget == UWPSubtarget.HoloLens +#else + EditorUserBuildSettings.wsaSubtarget == WSASubtarget.HoloLens +#endif // UNITY_2021_2_OR_NEWER + ) + { + canInstall = DevicePortalConnectionEnabled; + } + + return canInstall && Directory.Exists(BuildDeployPreferences.AbsoluteBuildDirectory) && !string.IsNullOrEmpty(PackageName); + } + } + + private static string PackageName { get; set; } + + private static bool IsHoloLensConnectedUsb + { + get + { + bool isConnected = false; + + if (USBDeviceListener.USBDevices != null) + { + if (USBDeviceListener.USBDevices.Any(device => device.Name.Equals("Microsoft HoloLens"))) + { + isConnected = true; + } + + SessionState.SetBool("HoloLensUsbConnected", isConnected); + } + else + { + isConnected = SessionState.GetBool("HoloLensUsbConnected", false); + } + + return isConnected; + } + } + + private static DeviceInfo CurrentConnection + { + get => UseRemoteTarget ? CurrentRemoteConnection : localConnection; + } + + private static DeviceInfo CurrentRemoteConnection + { + get + { + var connections = portalConnections?.Connections; + if (connections != null && CurrentRemoteConnectionIndex >= 0 && CurrentRemoteConnectionIndex < connections.Count) + { + return connections[CurrentRemoteConnectionIndex]; + } + + return null; + } + } + + private static int CurrentRemoteConnectionIndex + { + get => portalConnections != null ? portalConnections.CurrentConnectionIndex : -1; + set + { + var connections = portalConnections?.Connections; + if (connections != null && value >= 0 && value < connections.Count) + { + portalConnections.CurrentConnectionIndex = value; + } + } + } + + /// + /// Tracks whether the current UI preference is to target the local machine or remote machine for deployment. + /// Saves state for duration of current Unity session + /// + private static bool UseRemoteTarget + { + get => SessionState.GetBool(UseRemoteTargetSessionKey, false); + set => SessionState.SetBool(UseRemoteTargetSessionKey, value); + } + + #endregion Properties + + #region Fields + + private const float HALF_WIDTH = 256f; + + private string[] targetIps; + private readonly List windowsSdkVersions = new List(); + + private Vector2 buildSceneListScrollPosition; + private Vector2 deployBuildListScrollPosition; + private Vector2 appxBuildOptionsScrollPosition; + + private BuildDeployTab currentTab = BuildDeployTab.UnityBuildOptions; + private Action[] tabRenders; + + private static bool isBuilding; + private static bool isAppRunning; + + private static DevicePortalConnections portalConnections = null; + private static CancellationTokenSource appxCancellationTokenSource = null; + private static float appxProgressBarTimer = 0.0f; + private static DeviceInfo localConnection; + + private static bool lastTestConnectionSuccessful = false; + private static DeviceInfo lastTestConnectionTarget; + private static DateTime? lastTestConnectionTime = null; + +#if UNITY_2021_2_OR_NEWER + private static UWPSubtarget currentSubtarget = UWPSubtarget.AnyDevice; +#endif // UNITY_2021_2_OR_NEWER + + #endregion Fields + + #region Methods + + [MenuItem("Mixed Reality/Toolkit/Utilities/Build Window", false, 0)] + public static void OpenWindow() + { + // Dock it next to the Scene View. + var window = GetWindow(typeof(SceneView)); + window.titleContent = new GUIContent("Build Window", EditorGUIUtility.IconContent("CustomTool").image); + window.Show(); + } + + private void OnEnable() + { + LogoLightTheme = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(LogoLightThemeGuid)); + LogoDarkTheme = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(LogoDarkThemeGuid)); + + minSize = new Vector2(512, 256); + + tabRenders = new Action[] + { + () => { RenderUnityBuildView(); }, + () => { RenderAppxBuildView(); }, + () => { RenderDeployBuildView(); }, + }; + + LoadWindowsSdkPaths(); + UpdateBuilds(); + + DevicePortal.UseSSL = UwpBuildDeployPreferences.UseSSL; + + localConnection = JsonUtility.FromJson(UwpBuildDeployPreferences.LocalConnectionInfo); + + portalConnections = JsonUtility.FromJson(UwpBuildDeployPreferences.DevicePortalConnections); + + SaveRemotePortalConnections(); + } + + private void OnGUI() + { + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + //GUILayout.Label(EditorGUIUtility.isProSkin ? LogoDarkTheme : LogoLightTheme, GUILayout.MaxHeight(96f)); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.Space(3f); + + if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.WSAPlayer) + { + RenderStandaloneBuildView(); + } + else + { + RenderWSABuildView(); + } + } + + private void RenderWSABuildView() + { + RenderBuildDirectory(); + + using (new EditorGUILayout.HorizontalScope()) + { + using (new EditorGUI.DisabledGroupScope(!ShouldBuildSLNBeEnabled)) + { + if (GUILayout.Button(CanInstall ? BuildAllThenInstallLabel : BuildAllLabel, GUILayout.ExpandWidth(true))) + { + EditorApplication.delayCall += () => BuildAll(CanInstall); + } + } + + RenderPlayerSettingsButton(); + } + + EditorGUILayout.Space(); + + BuildDeployTab lastTab = currentTab; + currentTab = (BuildDeployTab)GUILayout.Toolbar(SessionState.GetInt(BuildWindowTabKey, (int)currentTab), TAB_NAMES); + if (currentTab != lastTab) + { + SessionState.SetInt(BuildWindowTabKey, (int)currentTab); + + if (currentTab == BuildDeployTab.DeployOptions) + { + UpdateBuilds(); + } + } + + using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) + { + tabRenders[(int)currentTab].Invoke(); + } + } + + private void RenderStandaloneBuildView() + { + RenderBuildDirectory(); + + using (new EditorGUILayout.HorizontalScope()) + { + RenderPlayerSettingsButton(); + + if (GUILayout.Button("Build Unity Project")) + { + EditorApplication.delayCall += () => UnityPlayerBuildTools.BuildUnityPlayer(new BuildInfo()); + } + + if (GUILayout.Button("Open Unity Build Window")) + { + GetWindow(Type.GetType("UnityEditor.BuildPlayerWindow,UnityEditor")); + } + } + } + + private void RenderUnityBuildView() + { +#if UNITY_2021_2_OR_NEWER + currentSubtarget = (UWPSubtarget)EditorGUILayout.Popup("Target Device", (int)currentSubtarget, TARGET_DEVICE_OPTIONS, GUILayout.Width(HALF_WIDTH)); +#else + EditorUserBuildSettings.wsaSubtarget = (WSASubtarget)EditorGUILayout.Popup("Target Device", (int)EditorUserBuildSettings.wsaSubtarget, TARGET_DEVICE_OPTIONS, GUILayout.Width(HALF_WIDTH)); +#endif // UNITY_2021_2_OR_NEWER + +#if !UNITY_2019_1_OR_NEWER + var curScriptingBackend = PlayerSettings.GetScriptingBackend(BuildTargetGroup.WSA); + if (curScriptingBackend == ScriptingImplementation.WinRTDotNET) + { + EditorGUILayout.HelpBox(".NET Scripting backend is deprecated in Unity 2018 and is removed in Unity 2019.", MessageType.Warning); + } + + var newScriptingBackend = (ScriptingImplementation)EditorGUILayout.IntPopup("Scripting Backend", (int)curScriptingBackend, SCRIPTING_BACKEND_NAMES, SCRIPTING_BACKEND_ENUMS, GUILayout.Width(HALF_WIDTH)); + if (newScriptingBackend != curScriptingBackend) + { + bool canUpdate = !Directory.Exists(BuildDeployPreferences.AbsoluteBuildDirectory); + if (EditorUtility.DisplayDialog("Attention!", + $"Build path contains project built with {curScriptingBackend.ToString()} scripting backend, while project wants to use {newScriptingBackend.ToString()} scripting backend.\n\nSwitching to a new scripting backend requires us to delete all the data currently in your build folder and rebuild the Unity Player!", + "Okay", "Cancel")) + { + Directory.Delete(BuildDeployPreferences.AbsoluteBuildDirectory, true); + canUpdate = true; + } + + if (canUpdate) + { + PlayerSettings.SetScriptingBackend(BuildTargetGroup.WSA, newScriptingBackend); + } + } + + // To prevent potential confusion, only show this when C# projects will be generated + if (EditorUserBuildSettings.wsaGenerateReferenceProjects && + PlayerSettings.GetScriptingBackend(BuildTargetGroup.WSA) == ScriptingImplementation.WinRTDotNET) + { + // Allow unsafe code + bool curAllowUnsafeCode = UwpBuildDeployPreferences.AllowUnsafeCode; + bool newAllowUnsafeCode = EditorGUILayout.ToggleLeft(AllowUnsafeCodeLabel, curAllowUnsafeCode); + if (newAllowUnsafeCode != curAllowUnsafeCode) + { + UwpBuildDeployPreferences.AllowUnsafeCode = newAllowUnsafeCode; + } + } + + // Generate C# Project References for debugging + if (PlayerSettings.GetScriptingBackend(BuildTargetGroup.WSA) == ScriptingImplementation.WinRTDotNET) + { + bool generateReferenceProjects = EditorUserBuildSettings.wsaGenerateReferenceProjects; + bool shouldGenerateProjects = EditorGUILayout.ToggleLeft(UseCSharpProjectsLabel, generateReferenceProjects); + if (shouldGenerateProjects != generateReferenceProjects) + { + EditorUserBuildSettings.wsaGenerateReferenceProjects = shouldGenerateProjects; + } + } +#endif // !UNITY_2019_1_OR_NEWER + + using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) + { + EditorGUILayout.LabelField("Scenes in Build", EditorStyles.boldLabel); + + using (var scrollView = new EditorGUILayout.ScrollViewScope(buildSceneListScrollPosition, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true))) + { + buildSceneListScrollPosition = scrollView.scrollPosition; + + using (new EditorGUI.DisabledGroupScope(true)) + { + var scenes = EditorBuildSettings.scenes; + for (int i = 0; i < scenes.Length; i++) + { + EditorGUILayout.ToggleLeft($"{i} {scenes[i].path}", scenes[i].enabled); + } + } + } + } + + using (new EditorGUILayout.HorizontalScope()) + { + using (new EditorGUI.DisabledGroupScope(!ShouldOpenSLNBeEnabled)) + { + RenderOpenVisualStudioButton(); + + if (GUILayout.Button("Build Unity Project")) + { + EditorApplication.delayCall += BuildUnityProject; + } + } + } + + EditorGUILayout.Space(); + } + + private void RenderAppxBuildView() + { + using (var scrollView = new EditorGUILayout.ScrollViewScope(appxBuildOptionsScrollPosition, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true))) + { + appxBuildOptionsScrollPosition = scrollView.scrollPosition; + + // SDK and MS Build Version (and save setting, if it's changed) + // Note that this is the 'Target SDK Version' which is required to physically build the + // code on a build machine, not the minimum platform version. + string currentSDKVersion = EditorUserBuildSettings.wsaUWPSDK; + + Version chosenSDKVersion = null; + for (var i = 0; i < windowsSdkVersions.Count; i++) + { + // windowsSdkVersions is sorted in ascending order, so we always take + // the highest SDK version that is above our minimum. + if (windowsSdkVersions[i] >= UwpBuildDeployPreferences.MIN_SDK_VERSION) + { + chosenSDKVersion = windowsSdkVersions[i]; + } + } + + EditorGUILayout.HelpBox($"Windows SDK Version: {currentSDKVersion}", MessageType.Info); + + // Throw exception if user has no Windows 10 SDK installed + if (chosenSDKVersion == null) + { + if (IsValidSdkInstalled) + { + Debug.LogError($"Unable to find the required Windows 10 SDK Target!\nPlease be sure to install the {UwpBuildDeployPreferences.MIN_SDK_VERSION} SDK from Visual Studio Installer."); + } + + EditorGUILayout.HelpBox($"Unable to find the required Windows 10 SDK Target!\nPlease be sure to install the {UwpBuildDeployPreferences.MIN_SDK_VERSION} SDK from Visual Studio Installer.", MessageType.Error); + IsValidSdkInstalled = false; + return; + } + + IsValidSdkInstalled = true; + + string newSDKVersion = chosenSDKVersion.ToString(); + if (!newSDKVersion.Equals(currentSDKVersion)) + { + EditorUserBuildSettings.wsaUWPSDK = newSDKVersion; + } + + string currentMinPlatformVersion = EditorUserBuildSettings.wsaMinUWPSDK; + if (string.IsNullOrWhiteSpace(currentMinPlatformVersion)) + { + // If the min platform version hasn't been specified, set it to the recommended value. + EditorUserBuildSettings.wsaMinUWPSDK = UwpBuildDeployPreferences.MIN_PLATFORM_VERSION.ToString(); + } + else if (UwpBuildDeployPreferences.MIN_PLATFORM_VERSION != new Version(currentMinPlatformVersion)) + { + // If the user has manually changed the minimum platform version in the 'Build Settings' window + // provide a warning that the generated application may not be deployable to older generation + // devices. We generally recommend setting to the lowest value and letting the app model's + // capability and versioning checks kick in for applications at runtime. + EditorGUILayout.HelpBox( + "Minimum platform version is set to a different value from the recommended value: " + + $"{UwpBuildDeployPreferences.MIN_PLATFORM_VERSION}, the generated app may not be deployable to older generation devices. " + + $"Consider updating the 'Minimum Platform Version' in the Build Settings window to match {UwpBuildDeployPreferences.MIN_PLATFORM_VERSION}", + MessageType.Warning); + } + + using (var c = new EditorGUI.ChangeCheckScope()) + { + EditorGUILayout.LabelField("Build Options", EditorStyles.boldLabel); + var newBuildConfigOption = (WSABuildType)EditorGUILayout.EnumPopup("Build Configuration", UwpBuildDeployPreferences.BuildConfigType, GUILayout.Width(HALF_WIDTH)); + UwpBuildDeployPreferences.BuildConfig = newBuildConfigOption.ToString().ToLower(); + + // Build Platform + int currentPlatformIndex = Array.IndexOf(ARCHITECTURE_OPTIONS, EditorUserBuildSettings.wsaArchitecture); + int buildPlatformIndex = EditorGUILayout.Popup("Build Platform", currentPlatformIndex, ARCHITECTURE_OPTIONS, GUILayout.Width(HALF_WIDTH)); + + // Platform Toolset + int currentPlatformToolsetIndex = Array.IndexOf(PLATFORM_TOOLSET_VALUES, UwpBuildDeployPreferences.PlatformToolset); + int newPlatformToolsetIndex = EditorGUILayout.Popup("Platform Toolset", currentPlatformToolsetIndex, PLATFORM_TOOLSET_NAMES, GUILayout.Width(HALF_WIDTH)); + + // Force rebuild + bool forceRebuildAppx = EditorGUILayout.ToggleLeft("Force Rebuild", UwpBuildDeployPreferences.ForceRebuild); + + // Multicore Appx Build + bool multicoreAppxBuildEnabled = EditorGUILayout.ToggleLeft("Multicore Build", UwpBuildDeployPreferences.MulticoreAppxBuildEnabled); + + EditorGUILayout.LabelField("Manifest Options", EditorStyles.boldLabel); + + // The 'Gaze Input' capability support was added for HL2 in the Windows SDK 18362, but + // existing versions of Unity don't have support for automatically adding the capability to the generated + // AppX manifest during the build. This option provides a mechanism for people using the + // MRTK build tools to auto-append this capability if desired, instead of having to manually + // do this each time on their own. + bool gazeInputCapabilityEnabled = EditorGUILayout.ToggleLeft(GazeInputCapabilityLabel, UwpBuildDeployPreferences.GazeInputCapabilityEnabled); + + // Enable Research Mode Capability + bool researchModeEnabled = EditorGUILayout.ToggleLeft(ResearchModeCapabilityLabel, UwpBuildDeployPreferences.ResearchModeCapabilityEnabled); + + // Don't draw the preview while building (when appxCancellationTokenSource will be non-null), + // since there's a null texture issue when Unity reloads the assets during a build + MixedRealityBuildPreferences.DrawAppLauncherModelField(appxCancellationTokenSource == null); + + // Draw the section for nuget executable path + EditorGUILayout.LabelField("Nuget Path (Optional)", EditorStyles.boldLabel); + + string nugetExecutablePath = EditorGUILayout.TextField(NugetPathLabel, UwpBuildDeployPreferences.NugetExecutablePath); + using (new EditorGUILayout.HorizontalScope()) + { + if (GUILayout.Button("Select Nuget Executable Path")) + { + nugetExecutablePath = EditorUtility.OpenFilePanel( + "Select Nuget Executable Path", "", "exe"); + } + if (GUILayout.Button("Use msbuild for Restore & Clear Path")) + { + nugetExecutablePath = ""; + } + } + if (c.changed) + { + UwpBuildDeployPreferences.PlatformToolset = PLATFORM_TOOLSET_VALUES[newPlatformToolsetIndex]; + EditorUserBuildSettings.wsaArchitecture = ARCHITECTURE_OPTIONS[buildPlatformIndex]; + UwpBuildDeployPreferences.GazeInputCapabilityEnabled = gazeInputCapabilityEnabled; + UwpBuildDeployPreferences.ResearchModeCapabilityEnabled = researchModeEnabled; + UwpBuildDeployPreferences.ForceRebuild = forceRebuildAppx; + UwpBuildDeployPreferences.MulticoreAppxBuildEnabled = multicoreAppxBuildEnabled; + UwpBuildDeployPreferences.NugetExecutablePath = nugetExecutablePath; + } + } + } + + EditorGUILayout.LabelField("Versioning Options", EditorStyles.boldLabel); + + using (new EditorGUILayout.HorizontalScope()) + { + using (var c = new EditorGUI.ChangeCheckScope()) + { + // Auto Increment version + bool incrementVersion = EditorGUILayout.ToggleLeft(AutoIncrementLabel, BuildDeployPreferences.IncrementBuildVersion); + + EditorGUILayout.LabelField(VersionNumberLabel, GUILayout.Width(96)); + Vector3 newVersion = Vector3.zero; + + newVersion.x = EditorGUILayout.IntField(PlayerSettings.WSA.packageVersion.Major); + newVersion.y = EditorGUILayout.IntField(PlayerSettings.WSA.packageVersion.Minor); + newVersion.z = EditorGUILayout.IntField(PlayerSettings.WSA.packageVersion.Build); + + if (c.changed) + { + BuildDeployPreferences.IncrementBuildVersion = incrementVersion; + PlayerSettings.WSA.packageVersion = new Version((int)newVersion.x, (int)newVersion.y, (int)newVersion.z, 0); + } + } + + using (new EditorGUI.DisabledGroupScope(true)) + { + EditorGUILayout.IntField(PlayerSettings.WSA.packageVersion.Revision); + } + } + + EditorGUILayout.Space(); + + if (appxCancellationTokenSource != null) + { + using (var progressBarRect = new EditorGUILayout.VerticalScope()) + { + appxProgressBarTimer = Mathf.Clamp01(Time.realtimeSinceStartup % 1.0f); + + EditorGUI.ProgressBar(progressBarRect.rect, appxProgressBarTimer, "Building AppX..."); + GUILayout.Space(16); + Repaint(); + } + } + + using (new EditorGUILayout.HorizontalScope()) + { + GUILayout.FlexibleSpace(); + + // Open AppX packages location + string appxDirectory = PlayerSettings.GetScriptingBackend(BuildTargetGroup.WSA) == ScriptingImplementation.IL2CPP ? $"/AppPackages/{PlayerSettings.productName}" : $"/{PlayerSettings.productName}/AppPackages"; + string appxBuildPath = Path.GetFullPath($"{BuildDeployPreferences.BuildDirectory}{appxDirectory}"); + + using (new EditorGUI.DisabledGroupScope(Builds.Count <= 0 || string.IsNullOrEmpty(appxBuildPath))) + { + if (GUILayout.Button("Open AppX Packages Location", GUILayout.Width(HALF_WIDTH))) + { + EditorApplication.delayCall += () => Process.Start("explorer.exe", $"/f /open,{appxBuildPath}"); + } + } + + if (appxCancellationTokenSource == null) + { + using (new EditorGUI.DisabledGroupScope(!ShouldBuildAppxBeEnabled)) + { + if (GUILayout.Button("Build AppX", GUILayout.Width(HALF_WIDTH))) + { + // Check if solution exists + string slnFilename = Path.Combine(BuildDeployPreferences.BuildDirectory, $"{PlayerSettings.productName}.sln"); + if (File.Exists(slnFilename)) + { + EditorApplication.delayCall += BuildAppx; + } + else if (EditorUtility.DisplayDialog("Solution Not Found", "We couldn't find the Visual Studio solution. Would you like to build it?", "Yes, Build Unity", "No")) + { + EditorApplication.delayCall += () => BuildAll(install: false); + } + } + } + } + else + { + if (GUILayout.Button("Cancel Build", GUILayout.Width(HALF_WIDTH))) + { + appxCancellationTokenSource.Cancel(); + } + } + } + } + + private void RenderDeployBuildView() + { + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField(TargetTypeLabel, GUILayout.Width(75)); + UseRemoteTarget = EditorGUILayout.Popup(UseRemoteTarget ? 1 : 0, LocalRemoteOptions, GUILayout.Width(100)) != 0; + GUILayout.FlexibleSpace(); + } + + using (new GUILayout.VerticalScope("box")) + { + if (UseRemoteTarget) + { + RenderRemoteConnections(); + + RenderSSLButtons(); + } + else + { + RenderLocalConnection(); + + if (IsHoloLensConnectedUsb) + { + using (new EditorGUI.DisabledGroupScope(!AreCredentialsValid(localConnection))) + { + if (GUILayout.Button("Discover HoloLens WiFi IP", GUILayout.Width(HALF_WIDTH))) + { + EditorApplication.delayCall += () => + { + DiscoverLocalHololensIP(); + }; + } + } + } + + RenderSSLButtons(); + } + + EditorGUILayout.Space(); + + RenderConnectionButtons(); + } + + EditorGUILayout.Space(); + + RenderBuildsList(); + } + + private void RenderSSLButtons() + { + using (new EditorGUILayout.HorizontalScope()) + { + bool useSSL = UwpBuildDeployPreferences.UseSSL; + bool newUseSSL = EditorGUILayout.ToggleLeft(UseSSLLabel, useSSL); + if (newUseSSL != useSSL) + { + UwpBuildDeployPreferences.UseSSL = newUseSSL; + DevicePortal.UseSSL = newUseSSL; + } + else if (UwpBuildDeployPreferences.UseSSL != DevicePortal.UseSSL) + { + DevicePortal.UseSSL = UwpBuildDeployPreferences.UseSSL; + } + + bool verifySSL = UwpBuildDeployPreferences.VerifySSL; + bool newVerifySSL = EditorGUILayout.ToggleLeft(VerifySSLLabel, verifySSL); + if (newVerifySSL != verifySSL) + { + UwpBuildDeployPreferences.VerifySSL = newVerifySSL; + DevicePortal.VerifySSLCertificates = verifySSL; + } + else if (UwpBuildDeployPreferences.VerifySSL != DevicePortal.VerifySSLCertificates) + { + DevicePortal.VerifySSLCertificates = UwpBuildDeployPreferences.VerifySSL; + } + + GUILayout.FlexibleSpace(); + } + } + + private void RenderConnectionButtons() + { + DeviceInfo currentConnection = CurrentConnection; + + bool canTestConnection = (!UseRemoteTarget || IsValidIpAddress(currentConnection.IP)) && AreCredentialsValid(currentConnection); + using (new EditorGUI.DisabledGroupScope(!canTestConnection)) + { + + using (new EditorGUILayout.HorizontalScope()) + { + if (GUILayout.Button("Test Connection", GUILayout.Width(128f))) + { + lastTestConnectionTime = null; + + EditorApplication.delayCall += async () => + { + lastTestConnectionSuccessful = await ConnectToDevice(); + lastTestConnectionTime = DateTime.UtcNow; + lastTestConnectionTarget = currentConnection; + }; + } + + if (lastTestConnectionTime != null) + { + string successStatus = lastTestConnectionSuccessful ? "Successful" : "Failed"; + EditorGUILayout.LabelField($"{successStatus} connection to {lastTestConnectionTarget.ToString()}, {lastTestConnectionTime.Value.GetRelativeTime()}"); + } + } + + EditorGUILayout.Space(); + + using (new EditorGUILayout.HorizontalScope()) + { + using (new EditorGUI.DisabledGroupScope(false)) + { + if (GUILayout.Button("Open Device Portal", GUILayout.Width(128f))) + { + EditorApplication.delayCall += () => OpenDevicePortal(); + } + } + + GUILayout.FlexibleSpace(); + } + } + } + + private void RenderLocalConnection() + { + using (var c = new EditorGUI.ChangeCheckScope()) + { + string target = IsHoloLensConnectedUsb ? HOLOLENS_USB : DeviceInfo.LocalMachine; + EditorGUILayout.LabelField(target, GUILayout.Width(HALF_WIDTH)); + + EditorGUILayout.LabelField(IPAddressLabel, new GUIContent(localConnection.IP), GUILayout.Width(HALF_WIDTH)); + + localConnection.User = EditorGUILayout.TextField(UsernameLabel, localConnection.User, GUILayout.Width(HALF_WIDTH)); + + localConnection.Password = EditorGUILayout.PasswordField(PasswordLabel, localConnection.Password, GUILayout.Width(HALF_WIDTH)); + + if (c.changed) + { + SaveLocalConnection(); + } + } + } + + private void RenderRemoteConnections() + { + using (var c = new EditorGUI.ChangeCheckScope()) + { + if (portalConnections.Connections.Count == 0) + { + AddRemoteConnection(); + } + + DeviceInfo currentConnection = CurrentRemoteConnection; + + using (new EditorGUILayout.HorizontalScope()) + { + CurrentRemoteConnectionIndex = EditorGUILayout.Popup(CurrentRemoteConnectionIndex, targetIps, GUILayout.Width(260)); + + if (GUILayout.Button(AddConnectionLabel, EditorStyles.miniButtonLeft, GUILayout.Width(20))) + { + AddRemoteConnection(); + } + + using (new EditorGUI.DisabledGroupScope(portalConnections.Connections.Count <= 1)) + { + if (GUILayout.Button(RemoveConnectionLabel, EditorStyles.miniButtonRight, GUILayout.Width(20))) + { + RemoveConnection(); + } + } + + GUILayout.FlexibleSpace(); + } + + if (IsHoloLensConnectedUsb && IsLocalConnection(currentConnection)) + { + EditorGUILayout.LabelField(HOLOLENS_USB); + } + + currentConnection.IP = EditorGUILayout.TextField(IPAddressLabel, currentConnection.IP, GUILayout.Width(HALF_WIDTH)); + + currentConnection.User = EditorGUILayout.TextField(UsernameLabel, currentConnection.User, GUILayout.Width(HALF_WIDTH)); + + currentConnection.Password = EditorGUILayout.PasswordField(PasswordLabel, currentConnection.Password, GUILayout.Width(HALF_WIDTH)); + + if (c.changed) + { + SaveRemotePortalConnections(); + } + } + } + + private void RenderBuildsList() + { + DeviceInfo currentConnection = CurrentConnection; + + using (new EditorGUILayout.HorizontalScope()) + { + if (GUILayout.Button(RefreshBuildsLabel)) + { + UpdateBuilds(); + } + + GUILayout.FlexibleSpace(); + } + + bool processAll = UwpBuildDeployPreferences.TargetAllConnections; + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUI.BeginChangeCheck(); + + processAll = EditorGUILayout.ToggleLeft(ExecuteOnAllDevicesLabel, processAll); + + bool fullReinstall = EditorGUILayout.ToggleLeft(AlwaysUninstallLabel, UwpBuildDeployPreferences.FullReinstall); + + GUILayout.FlexibleSpace(); + + if (EditorGUI.EndChangeCheck()) + { + UwpBuildDeployPreferences.TargetAllConnections = processAll; + UwpBuildDeployPreferences.FullReinstall = fullReinstall; + } + } + + EditorGUILayout.LabelField(string.Empty, GUI.skin.horizontalSlider); + + if (Builds.Count == 0) + { + EditorGUILayout.HelpBox("***No builds found in build directory", MessageType.Info); + } + else + { + using (new EditorGUILayout.VerticalScope(GUILayout.ExpandHeight(true))) + { + using (var scrollView = new EditorGUILayout.ScrollViewScope(deployBuildListScrollPosition, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true))) + { + deployBuildListScrollPosition = scrollView.scrollPosition; + + foreach (var fullBuildLocation in Builds) + { + if (!Directory.Exists(fullBuildLocation)) + { + continue; + } + + int lastBackslashIndex = fullBuildLocation.LastIndexOf("\\", StringComparison.Ordinal); + + var directoryDate = Directory.GetLastWriteTime(fullBuildLocation).ToString("yyyy/MM/dd HH:mm:ss"); + string packageName = fullBuildLocation.Substring(lastBackslashIndex + 1); + + using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox)) + { + using (new EditorGUI.DisabledGroupScope(!CanInstall)) + { + if (GUILayout.Button(InstallAppXLabel, GUILayout.Width(120))) + { + EditorApplication.delayCall += () => + { + ExecuteAction((DeviceInfo connection) + => InstallAppOnDeviceAsync(fullBuildLocation, connection)); + }; + } + + if (GUILayout.Button(UninstallAppXLabel, GUILayout.Width(120))) + { + EditorApplication.delayCall += () => + { + ExecuteAction((DeviceInfo connection) + => UninstallAppOnDeviceAsync(connection)); + }; + } + } + + bool canLaunchLocal = IsLocalConnection(currentConnection) && IsHoloLensConnectedUsb; + bool canLaunchRemote = DevicePortalConnectionEnabled && CanInstall; + + // Launch app... + bool launchAppEnabled = canLaunchLocal || canLaunchRemote; + using (new EditorGUI.DisabledGroupScope(!launchAppEnabled)) + { + if (isAppRunning) + { + if (GUILayout.Button(KillAppLabel, GUILayout.Width(96))) + { + ExecuteAction((DeviceInfo connection) + => KillAppOnDeviceAsync(connection)); + + isAppRunning = false; + } + } + else + { + if (GUILayout.Button(LaunchAppLabel, GUILayout.Width(96))) + { + ExecuteAction((DeviceInfo connection) + => LaunchAppOnDeviceAsync(connection)); + + isAppRunning = true; + } + } + } + + // Log file + string localLogPath = $"%USERPROFILE%\\AppData\\Local\\Packages\\{PlayerSettings.productName}\\TempState\\UnityPlayer.log"; + bool localLogExists = File.Exists(localLogPath); + + bool viewLogEnabled = localLogExists || canLaunchRemote || canLaunchLocal; + using (new EditorGUI.DisabledGroupScope(!viewLogEnabled)) + { + if (GUILayout.Button(ViewPlayerLogLabel, GUILayout.Width(126))) + { + EditorApplication.delayCall += () => + { + ExecuteAction((DeviceInfo connection) + => OpenLogFileForDeviceAsync(connection, localLogPath)); + }; + } + } + + EditorGUILayout.LabelField(new GUIContent($"{packageName} ({directoryDate})")); + } + } + } + } + } + } + + #endregion Methods + + #region Render Helpers + + private void RenderBuildDirectory() + { + using (new EditorGUILayout.HorizontalScope()) + { + string currentBuildDirectory = BuildDeployPreferences.BuildDirectory; + + EditorGUILayout.LabelField(BuildDirectoryLabel, GUILayout.Width(96)); + + using (new EditorGUI.DisabledGroupScope(true)) + { + EditorGUILayout.TextField(currentBuildDirectory, GUILayout.ExpandWidth(true)); + } + + if (GUILayout.Button(new GUIContent("Select Folder"), EditorStyles.miniButtonLeft, GUILayout.Width(100f))) + { + var fullBuildPath = Path.GetFullPath(EditorUtility.OpenFolderPanel("Select Build Directory", currentBuildDirectory, string.Empty)); + + // Temporary code as original design only allowed relative paths + string projectPath = Path.GetFullPath(Path.Combine(Application.dataPath, "../")); + + if (fullBuildPath.StartsWith(projectPath)) + { + BuildDeployPreferences.BuildDirectory = fullBuildPath.Replace(projectPath, string.Empty); + } + else + { + Debug.LogError("Build path must be relative to current Unity project"); + } + } + + RenderOpenBuildDirectoryButton(); + } + + EditorGUILayout.Space(); + } + + private static void RenderOpenVisualStudioButton() + { + if (GUILayout.Button("Open in Visual Studio", GUILayout.Width(HALF_WIDTH))) + { + string slnFilename = Path.Combine(BuildDeployPreferences.BuildDirectory, $"{PlayerSettings.productName}.sln"); + + if (File.Exists(slnFilename)) + { + EditorApplication.delayCall += () => Process.Start(new FileInfo(slnFilename).FullName); + } + else if (EditorUtility.DisplayDialog( + "Solution Not Found", + "We couldn't find the Project's Solution. Would you like to Build the project now?", + "Yes, Build", "No")) + { + EditorApplication.delayCall += BuildUnityProject; + } + } + } + + private static void RenderPlayerSettingsButton() + { + if (GUILayout.Button("Open Player Settings")) + { + Selection.activeObject = Unsupported.GetSerializedAssetInterfaceSingleton("PlayerSettings"); + } + } + + private static void RenderOpenBuildDirectoryButton() + { + using (new EditorGUI.DisabledGroupScope(!Directory.Exists(BuildDeployPreferences.AbsoluteBuildDirectory))) + { + if (GUILayout.Button("Open", EditorStyles.miniButtonRight, GUILayout.Width(100f))) + { + EditorApplication.delayCall += () => Process.Start(BuildDeployPreferences.AbsoluteBuildDirectory); + } + } + } + + #endregion + + #region Utilities + + private void RemoveConnection() + { + var connections = portalConnections?.Connections; + if (connections != null && connections.Count > 0) + { + portalConnections.Connections.RemoveAt(CurrentRemoteConnectionIndex); + CurrentRemoteConnectionIndex--; + SaveRemotePortalConnections(); + } + } + + private void AddRemoteConnection() + { + DeviceInfo currentConnection = CurrentRemoteConnection; + AddRemoteConnection(new DeviceInfo(EMPTY_IP_ADDRESS, currentConnection.User, currentConnection.Password)); + } + + private void AddRemoteConnection(DeviceInfo newDevice) + { + portalConnections.Connections.Add(newDevice); + CurrentRemoteConnectionIndex = portalConnections.Connections.Count - 1; + SaveRemotePortalConnections(); + } + + private async void DiscoverLocalHololensIP() + { + var machineName = await DevicePortal.GetMachineNameAsync(localConnection); + var networkInfo = await DevicePortal.GetIpConfigInfoAsync(localConnection); + if (machineName != null && networkInfo != null) + { + foreach (var adapter in networkInfo.Adapters) + { + if (adapter.Type.Contains(WifiAdapterType)) + { + foreach (var address in adapter.IpAddresses) + { + string ipAddress = address.IpAddress; + if (IsValidIpAddress(ipAddress) + && !portalConnections.Connections.Any(connection => connection.IP == ipAddress)) + { + Debug.Log($"Adding new IP {ipAddress} for local HoloLens {machineName.ComputerName} to remote connection list"); + + AddRemoteConnection(new DeviceInfo(ipAddress, + localConnection.User, + localConnection.Password, + machineName.ComputerName)); + + return; + } + } + } + } + + Debug.Log($"No new or valid WiFi IP Addresses found for local HoloLens {machineName.ComputerName}"); + } + } + + private async Task ConnectToDevice() + { + DeviceInfo currentConnection = CurrentConnection; + var machineName = await DevicePortal.GetMachineNameAsync(currentConnection); + if (machineName != null) + { + currentConnection.MachineName = machineName?.ComputerName; + SaveRemotePortalConnections(); + Debug.Log($"Successfully connected to device {machineName?.ComputerName} with IP {currentConnection.IP}"); + return true; + } + return false; + } + + /// + /// Builds the open Unity project for + /// BuildTarget.WSAPlayer. + /// + public static async void BuildUnityProject() + { + Debug.Assert(!isBuilding); + isBuilding = true; + + appxCancellationTokenSource = new CancellationTokenSource(); + await UwpPlayerBuildTools.BuildPlayer(BuildDeployPreferences.BuildDirectory, cancellationToken: appxCancellationTokenSource.Token); + appxCancellationTokenSource.Dispose(); + appxCancellationTokenSource = null; + + isBuilding = false; + } + + /// + /// Builds an AppX for the open Unity project for + /// BuildTarget.WSAPlayer. + /// + public static async void BuildAppx() + { + Debug.Assert(!isBuilding); + isBuilding = true; + + appxCancellationTokenSource = new CancellationTokenSource(); + + var buildInfo = new UwpBuildInfo + { + RebuildAppx = UwpBuildDeployPreferences.ForceRebuild, + Configuration = UwpBuildDeployPreferences.BuildConfig, + BuildPlatform = EditorUserBuildSettings.wsaArchitecture, + PlatformToolset = UwpBuildDeployPreferences.PlatformToolset, + OutputDirectory = BuildDeployPreferences.BuildDirectory, + AutoIncrement = BuildDeployPreferences.IncrementBuildVersion, + Multicore = UwpBuildDeployPreferences.MulticoreAppxBuildEnabled, + }; + + EditorAssemblyReloadManager.LockReloadAssemblies = true; + await UwpAppxBuildTools.BuildAppxAsync(buildInfo, appxCancellationTokenSource.Token); + EditorAssemblyReloadManager.LockReloadAssemblies = false; + appxCancellationTokenSource.Dispose(); + appxCancellationTokenSource = null; + + isBuilding = false; + } + + /// + /// Builds the open Unity project and its AppX for + /// BuildTarget.WSAPlayer. + /// + public static async void BuildAll(bool install = true) + { + Debug.Assert(!isBuilding); + isBuilding = true; + EditorAssemblyReloadManager.LockReloadAssemblies = true; + + appxCancellationTokenSource = new CancellationTokenSource(); + + // First build SLN + if (await UwpPlayerBuildTools.BuildPlayer(BuildDeployPreferences.BuildDirectory, false, appxCancellationTokenSource.Token)) + { + if (install) + { + string fullBuildLocation = CalcMostRecentBuild(); + + await ExecuteActionAsync((DeviceInfo connection) + => InstallAppOnDeviceAsync(fullBuildLocation, connection)); + } + } + + appxCancellationTokenSource.Dispose(); + appxCancellationTokenSource = null; + EditorAssemblyReloadManager.LockReloadAssemblies = false; + isBuilding = false; + } + + private static void UpdateBuilds() + { + Builds.Clear(); + + var curScriptingBackend = PlayerSettings.GetScriptingBackend(BuildTargetGroup.WSA); + string appxDirectory = curScriptingBackend == ScriptingImplementation.IL2CPP ? Path.Combine("AppPackages", PlayerSettings.productName) : Path.Combine(PlayerSettings.productName, "AppPackages"); + + try + { + AppPackageDirectories.Clear(); + string[] buildList = Directory.GetDirectories(BuildDeployPreferences.AbsoluteBuildDirectory, "*", SearchOption.AllDirectories); + foreach (string appBuild in buildList) + { + if (appBuild.Contains(appxDirectory) && !appBuild.Contains($"{appxDirectory}\\")) + { + AppPackageDirectories.AddRange(Directory.GetDirectories(appBuild)); + } + } + + IEnumerable selectedDirectories = + from string directory in AppPackageDirectories + orderby Directory.GetLastWriteTime(directory) descending + select Path.GetFullPath(directory); + Builds.AddRange(selectedDirectories); + } + catch (DirectoryNotFoundException) + { + // unused + } + + UpdatePackageName(); + } + + private static string CalcMostRecentBuild() + { + UpdateBuilds(); + DateTime mostRecent = DateTime.MinValue; + string mostRecentBuild = string.Empty; + + foreach (var fullBuildLocation in Builds) + { + DateTime directoryDate = Directory.GetLastWriteTime(fullBuildLocation); + + if (directoryDate > mostRecent) + { + mostRecentBuild = fullBuildLocation; + mostRecent = directoryDate; + } + } + + return mostRecentBuild; + } + + private void SaveLocalConnection() + { + UwpBuildDeployPreferences.LocalConnectionInfo = JsonUtility.ToJson(localConnection); + } + + private void SaveRemotePortalConnections() + { + targetIps = new string[portalConnections.Connections.Count]; + + for (int i = 0; i < targetIps.Length; i++) + { + var connection = portalConnections.Connections[i]; + + if (string.IsNullOrEmpty(connection.MachineName)) + { + targetIps[i] = connection.IP; + } + else + { + targetIps[i] = $"{connection.MachineName} - {connection.IP}"; + } + } + + UwpBuildDeployPreferences.DevicePortalConnections = JsonUtility.ToJson(portalConnections); + Repaint(); + } + + private static bool IsLocalConnection(DeviceInfo connection) + { + return connection.IP.Contains(DeviceInfo.LocalMachine) || + connection.IP.Contains(DeviceInfo.LocalIPAddress); + } + + private static bool AreCredentialsValid(DeviceInfo connection) + { + return !string.IsNullOrEmpty(connection.User) && + !string.IsNullOrEmpty(connection.IP) && + !string.IsNullOrEmpty(connection.Password); + } + + private static bool IsValidIpAddress(string ip) + { + if (string.IsNullOrEmpty(ip)) + { + return false; + } + + string ipAddr = ip; + + var portSegments = ip.Split(':'); + if (portSegments.Length > 2) + { + return false; + } + else if (portSegments.Length == 2) + { + if (!UInt16.TryParse(portSegments[1], out UInt16 result)) + { + return false; + } + + ipAddr = portSegments[0]; + } + + return ipAddr.Split('.').Length == 4 && !ipAddr.Contains(EMPTY_IP_ADDRESS) && + (IPAddress.TryParse(ipAddr, out IPAddress address) || ipAddr.Contains(DeviceInfo.LocalMachine)); + } + + private static string UpdatePackageName() + { + if (AppPackageDirectories.Count == 0) + { + return string.Empty; + } + + // Find the manifest + string[] manifests = Directory.GetFiles(BuildDeployPreferences.AbsoluteBuildDirectory, "Package.appxmanifest", SearchOption.AllDirectories); + + if (manifests.Length == 0) + { + Debug.LogError($"Unable to find manifest file for build (in path - {BuildDeployPreferences.AbsoluteBuildDirectory})"); + return string.Empty; + } + + string manifest = manifests[0]; + + // Parse it + using (var reader = new XmlTextReader(manifest)) + { + while (reader.Read()) + { + switch (reader.NodeType) + { + case XmlNodeType.Element: + if (reader.Name.Equals("identity", StringComparison.OrdinalIgnoreCase)) + { + while (reader.MoveToNextAttribute()) + { + if (reader.Name.Equals("name", StringComparison.OrdinalIgnoreCase)) + { + return PackageName = reader.Value; + } + } + } + + break; + } + } + } + + Debug.LogError($"Unable to find PackageFamilyName in manifest file ({manifest})"); + return string.Empty; + } + + private void LoadWindowsSdkPaths() + { + string win10KitsPath = WINDOWS_10_KITS_DEFAULT_PATH; +#if UNITY_EDITOR_WIN + // Windows 10 sdk might not be installed on C: drive. + // Try to detect the installation path by checking the registry. + try + { + var registryKey = Win32.Registry.LocalMachine.OpenSubKey(WINDOWS_10_KITS_PATH_REGISTRY_PATH); + var registryValue = registryKey.GetValue(WINDOWS_10_KITS_PATH_REGISTRY_KEY) as string; + win10KitsPath = Path.Combine(registryValue, WINDOWS_10_KITS_PATH_POSTFIX); + + if (!Directory.Exists(win10KitsPath)) + { + registryKey = Win32.Registry.LocalMachine.OpenSubKey(WINDOWS_10_KITS_PATH_ALTERNATE_REGISTRY_PATH); + registryValue = registryKey.GetValue(WINDOWS_10_KITS_PATH_REGISTRY_KEY) as string; + win10KitsPath = Path.Combine(registryValue, WINDOWS_10_KITS_PATH_POSTFIX); + + if (!Directory.Exists(win10KitsPath)) + { + Debug.LogWarning($"Could not find the Windows 10 SDK installation path via registry. Reverting to default path."); + win10KitsPath = WINDOWS_10_KITS_DEFAULT_PATH; + } + } + } + catch (Exception e) + { + Debug.LogWarning($"Could not find the Windows 10 SDK installation path via registry. Reverting to default path. {e}"); + win10KitsPath = WINDOWS_10_KITS_DEFAULT_PATH; + } +#endif + var windowsSdkPaths = Directory.GetDirectories(win10KitsPath); + for (int i = 0; i < windowsSdkPaths.Length; i++) + { + windowsSdkVersions.Add(new Version(windowsSdkPaths[i].Substring(windowsSdkPaths[i].LastIndexOf(@"\", StringComparison.Ordinal) + 1))); + } + + // There is no well-defined enumeration of Directory.GetDirectories, so the list + // is sorted prior to use later in this class. + windowsSdkVersions.Sort(); + } + + #endregion Utilities + + #region Device Portal Commands + + private static void OpenDevicePortal() + { + DevicePortal.OpenWebPortal(CurrentConnection); + } + + private static async void ExecuteAction(Func exec) + { + await ExecuteActionAsync(exec); + } + + private static async Task ExecuteActionAsync(Func exec) + { + List targetDevices; + + if (UwpBuildDeployPreferences.TargetAllConnections) + { + targetDevices = new List() { localConnection }; + targetDevices.AddRange(portalConnections.Connections); + } + else + { + targetDevices = new List() { CurrentConnection }; + } + + await ExecuteActionOnDevicesAsync(exec, targetDevices); + } + + private static async Task ExecuteActionOnDevicesAsync(Func exec, List devices) + { + var installTasks = new List(); + for (int i = 0; i < devices.Count; i++) + { + installTasks.Add(exec(devices[i])); + } + + await Task.WhenAll(installTasks); + } + + private static async Task InstallAppOnDeviceAsync(string buildPath, DeviceInfo targetDevice) + { + isAppRunning = false; + Debug.Log($"Initiating app install on device {targetDevice.ToString()}"); + + if (string.IsNullOrEmpty(PackageName)) + { + Debug.LogWarning("No Package Name Found"); + return; + } + + if (UwpBuildDeployPreferences.FullReinstall) + { + await UninstallAppOnDeviceAsync(targetDevice); + } + + if (IsLocalConnection(targetDevice) && (!IsHoloLensConnectedUsb || buildPath.Contains("x64"))) + { + FileInfo[] installerFiles = new DirectoryInfo(buildPath).GetFiles("Install.ps1"); + if (installerFiles.Length == 1) + { + var pInfo = new ProcessStartInfo + { + FileName = "powershell.exe", + CreateNoWindow = false, + Arguments = $"-executionpolicy bypass -File \"{installerFiles[0].FullName}\"" + }; + + var process = new Process { StartInfo = pInfo }; + + process.Start(); + } + + return; + } + + if (buildPath.Contains("x64")) + { + Debug.Log("Cannot install a x64 app on HoloLens"); + return; + } + + // Get the appx path + FileInfo[] files = new DirectoryInfo(buildPath).GetFiles("*.appx"); + files = files.Length == 0 ? new DirectoryInfo(buildPath).GetFiles("*.appxbundle") : files; + files = files.Length == 0 ? new DirectoryInfo(buildPath).GetFiles("*.msix") : files; + files = files.Length == 0 ? new DirectoryInfo(buildPath).GetFiles("*.msixbundle") : files; + + if (files.Length == 0) + { + Debug.LogErrorFormat("No APPX or MSIX found in folder build folder ({0})", buildPath); + return; + } + + await DevicePortal.InstallAppAsync(files[0].FullName, targetDevice); + } + + private static async Task UninstallAppOnDeviceAsync(DeviceInfo currentConnection) + { + isAppRunning = false; + + if (string.IsNullOrEmpty(PackageName)) + { + Debug.LogWarning("No Package Name Found"); + return; + } + + if (IsLocalConnection(currentConnection) && !IsHoloLensConnectedUsb) + { + var pInfo = new ProcessStartInfo + { + FileName = "powershell.exe", + CreateNoWindow = true, + Arguments = $"-windowstyle hidden -nologo Get-AppxPackage *{PackageName}* | Remove-AppxPackage" + }; + + var process = new Process { StartInfo = pInfo }; + process.Start(); + } + else + { + if (await DevicePortal.IsAppInstalledAsync(PackageName, currentConnection)) + { + await DevicePortal.UninstallAppAsync(PackageName, currentConnection); + } + } + } + + private static async Task LaunchAppOnDeviceAsync(DeviceInfo targetDevice) + { + if (string.IsNullOrEmpty(PackageName) || + IsLocalConnection(targetDevice) && !IsHoloLensConnectedUsb) + { + return; + } + + if (!await DevicePortal.IsAppRunningAsync(PackageName, targetDevice)) + { + isAppRunning = await DevicePortal.LaunchAppAsync(PackageName, targetDevice); + } + } + + private static async Task KillAppOnDeviceAsync(DeviceInfo targetDevice) + { + if (string.IsNullOrEmpty(PackageName) || + IsLocalConnection(targetDevice) && !IsHoloLensConnectedUsb) + { + return; + } + + if (await DevicePortal.IsAppRunningAsync(PackageName, targetDevice)) + { + isAppRunning = !await DevicePortal.StopAppAsync(PackageName, targetDevice); + } + } + + private static async Task OpenLogFileForDeviceAsync(DeviceInfo targetDevice, string localLogPath) + { + if (string.IsNullOrEmpty(PackageName)) + { + return; + } + + if (IsLocalConnection(targetDevice) && File.Exists(localLogPath)) + { + Process.Start(localLogPath); + return; + } + + if (!IsLocalConnection(targetDevice) || IsHoloLensConnectedUsb) + { + string logFilePath = await DevicePortal.DownloadLogFileAsync(PackageName, targetDevice); + + if (!string.IsNullOrEmpty(logFilePath)) + { + try + { + Process.Start(logFilePath); + } + catch (Exception e) + { + Debug.LogError($"Failed to open {logFilePath}!\n{e.Message}"); + } + } + + return; + } + + Debug.Log("No Log Found"); + } + + #endregion Device Portal Commands + } +} diff --git a/com.microsoft.mrtk.buildwindow/BuildDeployWindow.cs.meta b/com.microsoft.mrtk.buildwindow/BuildDeployWindow.cs.meta new file mode 100644 index 00000000000..dc446902716 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/BuildDeployWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0e458e04038177440bb68f19a0d598ae +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.microsoft.mrtk.buildwindow/ConfigurationStage.cs b/com.microsoft.mrtk.buildwindow/ConfigurationStage.cs new file mode 100644 index 00000000000..4422b67db99 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/ConfigurationStage.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.MixedReality.Toolkit.Editor.Inspectors")] +namespace Microsoft.MixedReality.Toolkit.Utilities.Editor +{ + /// + /// List of the stages of the project configurator + /// + internal enum ConfigurationStage + { + Init = 0, + SelectXRSDKPlugin = 100, + InstallOpenXR = 101, + InstallMSOpenXR = 102, + InstallBuiltinPlugin = 150, + ProjectConfiguration = 200, + ImportTMP = 300, + ShowExamples = 400, + Done = 500 + }; +} diff --git a/com.microsoft.mrtk.buildwindow/ConfigurationStage.cs.meta b/com.microsoft.mrtk.buildwindow/ConfigurationStage.cs.meta new file mode 100644 index 00000000000..43bc056897d --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/ConfigurationStage.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 606ad949ea9d452586f5f7fde697eaa6 +timeCreated: 1663171394 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/EditorAssemblyReloadManager.cs b/com.microsoft.mrtk.buildwindow/EditorAssemblyReloadManager.cs new file mode 100644 index 00000000000..ac73555270b --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/EditorAssemblyReloadManager.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using UnityEditor; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.Utilities.Editor +{ + public static class EditorAssemblyReloadManager + { + private static bool locked = false; + + /// + /// Locks the Editor's ability to reload assemblies. + /// + /// + /// This is useful for ensuring async tasks complete in the editor without having to worry if any script + /// changes that happen during the running task will cancel it when the editor re-compiles the assemblies. + /// + public static bool LockReloadAssemblies + { + set + { + locked = value; + + if (locked) + { + EditorApplication.LockReloadAssemblies(); + + if ((EditorWindow.focusedWindow != null) && + !Application.isBatchMode) + { + EditorWindow.focusedWindow.ShowNotification(new GUIContent("Assembly reloading temporarily paused.")); + } + } + else + { + EditorApplication.UnlockReloadAssemblies(); + EditorApplication.delayCall += () => AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate); + + if ((EditorWindow.focusedWindow != null) && + !Application.isBatchMode) + { + EditorWindow.focusedWindow.ShowNotification(new GUIContent("Assembly reloading resumed.")); + } + } + } + get => locked; + } + } +} diff --git a/com.microsoft.mrtk.buildwindow/EditorAssemblyReloadManager.cs.meta b/com.microsoft.mrtk.buildwindow/EditorAssemblyReloadManager.cs.meta new file mode 100644 index 00000000000..9e6e694f945 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/EditorAssemblyReloadManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 74c541b64cf647c7bac1587122d0ff28 +timeCreated: 1663170204 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/Extensions.meta b/com.microsoft.mrtk.buildwindow/Extensions.meta new file mode 100644 index 00000000000..4cd9a77a292 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Extensions.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 15379ca1bb754f42a05446dccbfec8f8 +timeCreated: 1663169587 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/Extensions/AwaiterExtensions.cs b/com.microsoft.mrtk.buildwindow/Extensions/AwaiterExtensions.cs new file mode 100644 index 00000000000..b3281d27a16 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Extensions/AwaiterExtensions.cs @@ -0,0 +1,403 @@ +// MIT License + +// Copyright(c) 2016 Modest Tree Media Inc + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using System.Text; +using System.Threading; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace Microsoft.MixedReality.Toolkit.Utilities +{ + /// + /// We could just add a generic GetAwaiter to YieldInstruction and CustomYieldInstruction + /// but instead we add specific methods to each derived class to allow for return values + /// that make the most sense for the specific instruction type. + /// + public static class AwaiterExtensions + { + public static SimpleCoroutineAwaiter GetAwaiter(this WaitForSeconds instruction) + { + return GetAwaiterReturnVoid(instruction); + } + + public static SimpleCoroutineAwaiter GetAwaiter(this WaitForUpdate instruction) + { + return GetAwaiterReturnVoid(instruction); + } + + public static SimpleCoroutineAwaiter GetAwaiter(this WaitForEndOfFrame instruction) + { + return GetAwaiterReturnVoid(instruction); + } + + public static SimpleCoroutineAwaiter GetAwaiter(this WaitForFixedUpdate instruction) + { + return GetAwaiterReturnVoid(instruction); + } + + public static SimpleCoroutineAwaiter GetAwaiter(this WaitForSecondsRealtime instruction) + { + return GetAwaiterReturnVoid(instruction); + } + + public static SimpleCoroutineAwaiter GetAwaiter(this WaitUntil instruction) + { + return GetAwaiterReturnVoid(instruction); + } + + public static SimpleCoroutineAwaiter GetAwaiter(this WaitWhile instruction) + { + return GetAwaiterReturnVoid(instruction); + } + + public static SimpleCoroutineAwaiter GetAwaiter(this AsyncOperation instruction) + { + return GetAwaiterReturnSelf(instruction); + } + + public static SimpleCoroutineAwaiter GetAwaiter(this ResourceRequest instruction) + { + var awaiter = new SimpleCoroutineAwaiter(); + RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( + InstructionWrappers.ResourceRequest(awaiter, instruction))); + return awaiter; + } + + public static SimpleCoroutineAwaiter GetAwaiter(this AssetBundleCreateRequest instruction) + { + var awaiter = new SimpleCoroutineAwaiter(); + RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( + InstructionWrappers.AssetBundleCreateRequest(awaiter, instruction))); + return awaiter; + } + + public static SimpleCoroutineAwaiter GetAwaiter(this AssetBundleRequest instruction) + { + var awaiter = new SimpleCoroutineAwaiter(); + RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( + InstructionWrappers.AssetBundleRequest(awaiter, instruction))); + return awaiter; + } + + public static SimpleCoroutineAwaiter GetAwaiter(this IEnumerator coroutine) + { + var awaiter = new SimpleCoroutineAwaiter(); + RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( + new CoroutineWrapper(coroutine, awaiter).Run())); + return awaiter; + } + + public static SimpleCoroutineAwaiter GetAwaiter(this IEnumerator coroutine) + { + var awaiter = new SimpleCoroutineAwaiter(); + RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( + new CoroutineWrapper(coroutine, awaiter).Run())); + return awaiter; + } + + private static SimpleCoroutineAwaiter GetAwaiterReturnVoid(object instruction) + { + var awaiter = new SimpleCoroutineAwaiter(); + RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( + InstructionWrappers.ReturnVoid(awaiter, instruction))); + return awaiter; + } + + private static SimpleCoroutineAwaiter GetAwaiterReturnSelf(T instruction) + { + var awaiter = new SimpleCoroutineAwaiter(); + RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( + InstructionWrappers.ReturnSelf(awaiter, instruction))); + return awaiter; + } + + private static void RunOnUnityScheduler(Action action) + { + if (SynchronizationContext.Current == SyncContextUtility.UnitySynchronizationContext) + { + action(); + } + else + { + // Make sure there is a running instance of AsyncCoroutineRunner before calling AsyncCoroutineRunner.Post + // If not warn the user. Note we cannot call AsyncCoroutineRunner.Instance here as that getter contains + // calls to Unity functions that can only be run on the Unity thread + if (!AsyncCoroutineRunner.IsInstanceRunning) + { + Debug.LogWarning("There is no active AsyncCoroutineRunner when an action is posted. Place a GameObject " + + "at the root of the scene and attach the AsyncCoroutineRunner script to make it function properly."); + } + AsyncCoroutineRunner.Post(action); + } + } + + /// + /// Processes Coroutine and notifies completion with result. + /// + /// The result type. + public class SimpleCoroutineAwaiter : INotifyCompletion + { + private Exception exception; + private Action continuation; + private T result; + + public bool IsCompleted { get; private set; } + + public T GetResult() + { + Debug.Assert(IsCompleted); + + if (exception != null) + { + ExceptionDispatchInfo.Capture(exception).Throw(); + } + + return result; + } + + public void Complete(T taskResult, Exception e) + { + Debug.Assert(!IsCompleted); + + IsCompleted = true; + exception = e; + result = taskResult; + + // Always trigger the continuation on the unity thread + // when awaiting on unity yield instructions. + if (continuation != null) + { + RunOnUnityScheduler(continuation); + } + } + + void INotifyCompletion.OnCompleted(Action notifyContinuation) + { + Debug.Assert(continuation == null); + Debug.Assert(!IsCompleted); + + continuation = notifyContinuation; + } + } + + /// + /// Processes Coroutine and notifies completion. + /// + public class SimpleCoroutineAwaiter : INotifyCompletion + { + private Exception exception; + private Action continuation; + + public bool IsCompleted { get; private set; } + + public void GetResult() + { + Debug.Assert(IsCompleted); + + if (exception != null) + { + ExceptionDispatchInfo.Capture(exception).Throw(); + } + } + + public void Complete(Exception e) + { + Debug.Assert(!IsCompleted); + + IsCompleted = true; + exception = e; + + // Always trigger the continuation on the unity thread + // when awaiting on unity yield instructions. + if (continuation != null) + { + RunOnUnityScheduler(continuation); + } + } + + void INotifyCompletion.OnCompleted(Action notifyContinuation) + { + Debug.Assert(continuation == null); + Debug.Assert(!IsCompleted); + + continuation = notifyContinuation; + } + } + + private class CoroutineWrapper + { + private readonly SimpleCoroutineAwaiter awaiter; + private readonly Stack processStack; + + public CoroutineWrapper(IEnumerator coroutine, SimpleCoroutineAwaiter awaiter) + { + processStack = new Stack(); + processStack.Push(coroutine); + this.awaiter = awaiter; + } + + public IEnumerator Run() + { + while (true) + { + var topWorker = processStack.Peek(); + + bool isDone; + + try + { + isDone = !topWorker.MoveNext(); + } + catch (Exception e) + { + // The IEnumerators we have in the process stack do not tell us the + // actual names of the coroutine methods but it does tell us the objects + // that the IEnumerators are associated with, so we can at least try + // adding that to the exception output + var objectTrace = GenerateObjectTrace(processStack); + awaiter.Complete(default(T), objectTrace.Any() ? new Exception(GenerateObjectTraceMessage(objectTrace), e) : e); + + yield break; + } + + if (isDone) + { + processStack.Pop(); + + if (processStack.Count == 0) + { + awaiter.Complete((T)topWorker.Current, null); + yield break; + } + } + + // We could just yield return nested IEnumerator's here but we choose to do + // our own handling here so that we can catch exceptions in nested coroutines + // instead of just top level coroutine + if (topWorker.Current is IEnumerator item) + { + processStack.Push(item); + } + else + { + // Return the current value to the unity engine so it can handle things like + // WaitForSeconds, WaitToEndOfFrame, etc. + yield return topWorker.Current; + } + } + } + + private static string GenerateObjectTraceMessage(List objTrace) + { + var result = new StringBuilder(); + + foreach (var objType in objTrace) + { + if (result.Length != 0) + { + result.Append(" -> "); + } + + result.Append(objType); + } + + result.AppendLine(); + return $"Unity Coroutine Object Trace: {result}"; + } + + private static List GenerateObjectTrace(IEnumerable enumerators) + { + var objTrace = new List(); + + foreach (var enumerator in enumerators) + { + // NOTE: This only works with scripting engine 4.6 + // And could easily stop working with unity updates + var field = enumerator.GetType().GetField("$this", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); + + if (field == null) + { + continue; + } + + var obj = field.GetValue(enumerator); + + if (obj == null) + { + continue; + } + + var objType = obj.GetType(); + + if (!objTrace.Any() || objType != objTrace.Last()) + { + objTrace.Add(objType); + } + } + + objTrace.Reverse(); + return objTrace; + } + } + + private static class InstructionWrappers + { + public static IEnumerator ReturnVoid(SimpleCoroutineAwaiter awaiter, object instruction) + { + // For simple instructions we assume that they don't throw exceptions + yield return instruction; + awaiter.Complete(null); + } + + public static IEnumerator AssetBundleCreateRequest(SimpleCoroutineAwaiter awaiter, AssetBundleCreateRequest instruction) + { + yield return instruction; + awaiter.Complete(instruction.assetBundle, null); + } + + public static IEnumerator ReturnSelf(SimpleCoroutineAwaiter awaiter, T instruction) + { + yield return instruction; + awaiter.Complete(instruction, null); + } + + public static IEnumerator AssetBundleRequest(SimpleCoroutineAwaiter awaiter, AssetBundleRequest instruction) + { + yield return instruction; + awaiter.Complete(instruction.asset, null); + } + + public static IEnumerator ResourceRequest(SimpleCoroutineAwaiter awaiter, ResourceRequest instruction) + { + yield return instruction; + awaiter.Complete(instruction.asset, null); + } + } + } +} diff --git a/com.microsoft.mrtk.buildwindow/Extensions/AwaiterExtensions.cs.meta b/com.microsoft.mrtk.buildwindow/Extensions/AwaiterExtensions.cs.meta new file mode 100644 index 00000000000..2e6a63c2970 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Extensions/AwaiterExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: dce2b4774333420584406cbbfcac9791 +timeCreated: 1663172106 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/Extensions/DateTimeExtensions.cs b/com.microsoft.mrtk.buildwindow/Extensions/DateTimeExtensions.cs new file mode 100644 index 00000000000..0d9ebe68916 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Extensions/DateTimeExtensions.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit +{ + /// + /// Extensions. + /// + public static class DateTimeExtensions + { + /// + /// Gets string literal for relative time from now since the DateTime provided. String output is in most appropriate "x time units ago" + /// Example: If DateTime provided is 30 seconds before now, then result will be "30 seconds ago" + /// + /// DateTime in UTC to compare against DateTime.UtcNow + /// Encoded string. + public static string GetRelativeTime(this DateTime time) + { + var delta = new TimeSpan(DateTime.UtcNow.Ticks - time.Ticks); + + if (Math.Abs(delta.TotalDays) > 1.0) + { + return (int)Math.Abs(delta.TotalDays) + " days ago"; + } + else if (Math.Abs(delta.TotalHours) > 1.0) + { + return (int)Math.Abs(delta.TotalHours) + " hours ago"; + } + else if (Math.Abs(delta.TotalMinutes) > 1.0) + { + return (int)Math.Abs(delta.TotalMinutes) + " minutes ago"; + } + else + { + return (int)Math.Abs(delta.TotalSeconds) + " seconds ago"; + } + } + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/Extensions/DateTimeExtensions.cs.meta b/com.microsoft.mrtk.buildwindow/Extensions/DateTimeExtensions.cs.meta new file mode 100644 index 00000000000..ad0ba1f7ab2 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Extensions/DateTimeExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 56e574316f4245908a20dfeb11fdfc1d +timeCreated: 1663169594 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/Extensions/ProcessExtensions.cs b/com.microsoft.mrtk.buildwindow/Extensions/ProcessExtensions.cs new file mode 100644 index 00000000000..45ea06e5666 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Extensions/ProcessExtensions.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#if UNITY_EDITOR || !UNITY_WSA +using Microsoft.MixedReality.Toolkit.Utilities; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.MixedReality.Toolkit +{ + /// + /// Process Extension class. + /// + public static class ProcessExtensions + { + /// + /// Starts a process asynchronously. + /// + /// This Process. + /// The process executable to run. + /// The Process arguments. + /// Should output debug code to Editor Console? + /// + public static async Task StartProcessAsync(this Process process, string fileName, string args, bool showDebug = false, CancellationToken cancellationToken = default) + { + return await StartProcessAsync(process, new ProcessStartInfo + { + FileName = fileName, + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + Arguments = args + }, showDebug, cancellationToken); + } + + /// + /// Starts a process asynchronously. + /// + /// The provided Process Start Info must not use shell execution, and should redirect the standard output and errors. + /// This Process. + /// The Process start info. + /// Should output debug code to Editor Console? + /// + public static async Task StartProcessAsync(this Process process, ProcessStartInfo startInfo, bool showDebug = false, CancellationToken cancellationToken = default) + { + Debug.Assert(!startInfo.UseShellExecute, "Process Start Info must not use shell execution."); + Debug.Assert(startInfo.RedirectStandardOutput, "Process Start Info must redirect standard output."); + Debug.Assert(startInfo.RedirectStandardError, "Process Start Info must redirect standard errors."); + + process.StartInfo = startInfo; + process.EnableRaisingEvents = true; + + var processResult = new TaskCompletionSource(); + var errorCodeResult = new TaskCompletionSource(); + var errorList = new List(); + var outputCodeResult = new TaskCompletionSource(); + var outputList = new List(); + + process.Exited += OnProcessExited; + process.ErrorDataReceived += OnErrorDataReceived; + process.OutputDataReceived += OnOutputDataReceived; + + async void OnProcessExited(object sender, EventArgs args) + { + processResult.TrySetResult(new ProcessResult(process.ExitCode, await errorCodeResult.Task, await outputCodeResult.Task)); + process.Close(); + process.Dispose(); + } + + void OnErrorDataReceived(object sender, DataReceivedEventArgs args) + { + if (args.Data != null) + { + errorList.Add(args.Data); + + if (!showDebug) + { + return; + } + + UnityEngine.Debug.LogError(args.Data); + } + else + { + errorCodeResult.TrySetResult(errorList.ToArray()); + } + } + + void OnOutputDataReceived(object sender, DataReceivedEventArgs args) + { + if (args.Data != null) + { + outputList.Add(args.Data); + + if (!showDebug) + { + return; + } + + UnityEngine.Debug.Log(args.Data); + } + else + { + outputCodeResult.TrySetResult(outputList.ToArray()); + } + } + + if (!process.Start()) + { + if (showDebug) + { + UnityEngine.Debug.LogError("Failed to start process!"); + } + + processResult.TrySetResult(new ProcessResult(process.ExitCode, new[] { "Failed to start process!" }, null)); + } + else + { + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + CancellationWatcher(process); + } + + async void CancellationWatcher(Process _process) + { + await Task.Run(() => + { + try + { + while (!_process.HasExited) + { + if (cancellationToken.IsCancellationRequested) + { + _process.Kill(); + } + } + } + catch + { + // ignored + } + }); + } + + return await processResult.Task; + } + } +} +#endif diff --git a/com.microsoft.mrtk.buildwindow/Extensions/ProcessExtensions.cs.meta b/com.microsoft.mrtk.buildwindow/Extensions/ProcessExtensions.cs.meta new file mode 100644 index 00000000000..b749ebde039 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Extensions/ProcessExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d500072f8d4f4ddb8ede274bb8d42fa1 +timeCreated: 1663170964 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/Extensions/StringExtensions.cs b/com.microsoft.mrtk.buildwindow/Extensions/StringExtensions.cs new file mode 100644 index 00000000000..c52fd180203 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Extensions/StringExtensions.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Text; + +namespace Microsoft.MixedReality.Toolkit +{ + /// + /// Extensions. + /// + public static class StringExtensions + { + /// + /// Encodes the string to base 64 ASCII. + /// + /// String to encode. + /// Encoded string. + public static string EncodeTo64(this string toEncode) + { + byte[] toEncodeAsBytes = Encoding.ASCII.GetBytes(toEncode); + return Convert.ToBase64String(toEncodeAsBytes); + } + + /// + /// Decodes string from base 64 ASCII. + /// + /// String to decode. + /// Decoded string. + public static string DecodeFrom64(this string encodedData) + { + byte[] encodedDataAsBytes = Convert.FromBase64String(encodedData); + return Encoding.ASCII.GetString(encodedDataAsBytes); + } + + /// + /// Capitalize the first character and add a space before + /// each capitalized letter (except the first character). + /// + public static string ToProperCase(this string value) + { + // If there are 0 or 1 characters, just return the string. + if (value == null) { return value; } + if (value.Length < 2) { return value.ToUpper(); } + // If there's already spaces in the string, return. + if (value.Contains(" ")) { return value; } + + // Start with the first character. + string result = value.Substring(0, 1).ToUpper(); + + // Add the remaining characters. + for (int i = 1; i < value.Length; i++) + { + if (char.IsLetter(value[i]) && + char.IsUpper(value[i])) + { + // Add a space if the previous character is not upper-case. + // e.g. "LeftHand" -> "Left Hand" + if (i != 1 && // First character is upper-case in result. + (!char.IsLetter(value[i - 1]) || char.IsLower(value[i - 1]))) + { + result += " "; + } + // If previous character is upper-case, only add space if the next + // character is lower-case. Otherwise assume this character to be inside + // an acronym. + // e.g. "OpenVRLeftHand" -> "Open VR Left Hand" + else if (i < value.Length - 1 && + char.IsLetter(value[i + 1]) && char.IsLower(value[i + 1])) + { + result += " "; + } + } + + result += value[i]; + } + + return result; + } + + /// + /// Ensures directory separator chars in provided string are platform independent. Given path might use \ or / but not all platforms support both. + /// + public static string NormalizeSeparators(this string path) + => path?.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); + } +} diff --git a/com.microsoft.mrtk.buildwindow/Extensions/StringExtensions.cs.meta b/com.microsoft.mrtk.buildwindow/Extensions/StringExtensions.cs.meta new file mode 100644 index 00000000000..a33fda59f6a --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Extensions/StringExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b1ca06febb0f401ab8eb6ff886068a37 +timeCreated: 1663170344 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/MRTK.Build.asmdef b/com.microsoft.mrtk.buildwindow/MRTK.Build.asmdef new file mode 100644 index 00000000000..197414dfe4d --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/MRTK.Build.asmdef @@ -0,0 +1,18 @@ +{ + "name": "Microsoft.MixedReality.Toolkit.Build", + "rootNamespace": "Microsoft.MixedReality.Toolkit.Build", + "references": [ + "GUID:a42927d1d4a3b4cda9b076a7adecb9cc" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/MRTK.Build.asmdef.meta b/com.microsoft.mrtk.buildwindow/MRTK.Build.asmdef.meta new file mode 100644 index 00000000000..c44b30de39b --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/MRTK.Build.asmdef.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7b002003e07c4631b7c7990008658cfe +timeCreated: 1663113002 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/Preferences.meta b/com.microsoft.mrtk.buildwindow/Preferences.meta new file mode 100644 index 00000000000..2e34167c20c --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Preferences.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9fc079c78f1e40968d5f1c180082b1b5 +timeCreated: 1663170636 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/Preferences/EditorPreferences.cs b/com.microsoft.mrtk.buildwindow/Preferences/EditorPreferences.cs new file mode 100644 index 00000000000..92e2cd1d7f0 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Preferences/EditorPreferences.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using UnityEditor; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.Utilities.Editor +{ + /// + /// Convenience class for setting Editor Preferences with Application.productName as key prefix. + /// + public static class EditorPreferences + { + /// + /// Set the saved from to EditorPrefs. + /// + public static void Set(string key, string value) + { + Debug.Assert(!string.IsNullOrWhiteSpace(key)); + EditorPrefs.SetString($"{Application.productName}_{key}", value); + } + + /// + /// Set the saved from to EditorPrefs. + /// + public static void Set(string key, bool value) + { + Debug.Assert(!string.IsNullOrWhiteSpace(key)); + EditorPrefs.SetBool($"{Application.productName}_{key}", value); + } + + /// + /// Set the saved from the EditorPrefs. + /// + public static void Set(string key, float value) + { + Debug.Assert(!string.IsNullOrWhiteSpace(key)); + EditorPrefs.SetFloat($"{Application.productName}_{key}", value); + } + + /// + /// Set the saved from theEditorPrefs. + /// + public static void Set(string key, int value) + { + Debug.Assert(!string.IsNullOrWhiteSpace(key)); + EditorPrefs.SetInt($"{Application.productName}_{key}", value); + } + + /// + /// Get the saved from theEditorPrefs. + /// + public static string Get(string key, string defaultValue) + { + Debug.Assert(!string.IsNullOrWhiteSpace(key)); + + if (EditorPrefs.HasKey($"{Application.productName}_{key}")) + { + return EditorPrefs.GetString($"{Application.productName}_{key}"); + } + + EditorPrefs.SetString($"{Application.productName}_{key}", defaultValue); + return defaultValue; + } + + /// + /// Get the saved from the EditorPrefs. + /// + public static bool Get(string key, bool defaultValue) + { + Debug.Assert(!string.IsNullOrWhiteSpace(key)); + + if (EditorPrefs.HasKey($"{Application.productName}_{key}")) + { + return EditorPrefs.GetBool($"{Application.productName}_{key}"); + } + + EditorPrefs.SetBool($"{Application.productName}_{key}", defaultValue); + return defaultValue; + } + + /// + /// Get the saved from the EditorPrefs. + /// + public static float Get(string key, float defaultValue) + { + Debug.Assert(!string.IsNullOrWhiteSpace(key)); + + if (EditorPrefs.HasKey($"{Application.productName}_{key}")) + { + return EditorPrefs.GetFloat($"{Application.productName}_{key}"); + } + + EditorPrefs.SetFloat($"{Application.productName}_{key}", defaultValue); + return defaultValue; + } + + /// + /// Get the saved from the EditorPrefs. + /// + public static int Get(string key, int defaultValue) + { + Debug.Assert(!string.IsNullOrWhiteSpace(key)); + + if (EditorPrefs.HasKey($"{Application.productName}_{key}")) + { + return EditorPrefs.GetInt($"{Application.productName}_{key}"); + } + + EditorPrefs.SetInt($"{Application.productName}_{key}", defaultValue); + return defaultValue; + } + } +} diff --git a/com.microsoft.mrtk.buildwindow/Preferences/EditorPreferences.cs.meta b/com.microsoft.mrtk.buildwindow/Preferences/EditorPreferences.cs.meta new file mode 100644 index 00000000000..d8796e64a8f --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Preferences/EditorPreferences.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6ce68d4730b74612a773f1e794e19030 +timeCreated: 1663170636 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/Preferences/MixedRealityProjectPreferences.cs b/com.microsoft.mrtk.buildwindow/Preferences/MixedRealityProjectPreferences.cs new file mode 100644 index 00000000000..a8b02a889ab --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Preferences/MixedRealityProjectPreferences.cs @@ -0,0 +1,279 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.MixedReality.Toolkit.Utilities.Editor; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.Editor +{ + /// + /// MRTK project preferences access and inspector rendering logic + /// + public static class MixedRealityProjectPreferences + { + #region Lock Profile Preferences + + private static readonly GUIContent LockContent = new GUIContent("Lock SDK profiles", "Locks the SDK profiles from being edited."); + private const string LOCK_KEY = "_MixedRealityToolkit_Editor_LockProfiles"; + private static bool lockPrefLoaded; + private static bool lockProfiles; + + /// + /// Should the default profile inspectors be disabled to prevent editing? + /// + public static bool LockProfiles + { + get + { + if (!lockPrefLoaded) + { + lockProfiles = ProjectPreferences.Get(LOCK_KEY, true); + lockPrefLoaded = true; + } + + return lockProfiles; + } + set => ProjectPreferences.Set(LOCK_KEY, lockProfiles = value); + } + + #endregion Lock Profile Preferences + + #region Ignore startup settings prompt + + private static readonly GUIContent IgnoreContent = new GUIContent("Ignore MRTK project configurator", "Prevents settings dialog popup from showing."); + private const string IGNORE_KEY = "_MixedRealityToolkit_Editor_IgnoreSettingsPrompts"; + private static bool ignorePrefLoaded; + private static bool ignoreSettingsPrompt; + + /// + /// Should the project configurator show when the project isn't configured according to MRTK's recommendations? + /// + public static bool IgnoreSettingsPrompt + { + get + { + if (!ignorePrefLoaded) + { + ignoreSettingsPrompt = ProjectPreferences.Get(IGNORE_KEY, false); + ignorePrefLoaded = true; + } + + return ignoreSettingsPrompt; + } + set => ProjectPreferences.Set(IGNORE_KEY, ignoreSettingsPrompt = value); + } + + #endregion Ignore startup settings prompt + + #region Configurator state + + private const string CONFIG_KEY = "_MixedRealityToolkit_Editor_ConfiguratorState"; + private static bool configuratorStateLoaded; + private static int configuratorSate; + + /// + /// Should the project configurator show when the project isn't configured according to MRTK's recommendations? + /// + internal static ConfigurationStage ConfiguratorState + { + get + { + if (!configuratorStateLoaded) + { + configuratorSate = ProjectPreferences.Get(CONFIG_KEY, 0); + configuratorStateLoaded = true; + } + + return (ConfigurationStage)configuratorSate; + } + set => ProjectPreferences.Set(CONFIG_KEY, configuratorSate = (int)value); + } + + #endregion Configurator state + + #region Auto-Enable UWP Capabilities + + private static readonly GUIContent AutoEnableCapabilitiesContent = new GUIContent("Auto-enable UWP capabilities", "When this setting is enabled, MRTK services requiring particular UWP capabilities will be auto-enabled in Publishing Settings.\n\nOnly valid for UWP Build Target projects.\n\nUWP Capabilities can be viewed under Player Settings > Publishing Settings."); + private const string AUTO_ENABLE_CAPABILITIES_KEY = "_MixedRealityToolkit_Editor_AutoEnableUWPCapabilities"; + private static bool autoEnabledCapabilitiesPrefLoaded; + private static bool autoEnabledCapabilitiesSettingsPrompt; + + /// + /// Should the UWP capabilities required by MRTK services be auto-enabled in Publishing Settings? + /// + /// Only valid for UWP Build Target projects. UWP Capabilities can be viewed under Player Settings > Publishing Settings. + public static bool AutoEnableUWPCapabilities + { + get + { + if (!autoEnabledCapabilitiesPrefLoaded) + { + autoEnabledCapabilitiesSettingsPrompt = ProjectPreferences.Get(AUTO_ENABLE_CAPABILITIES_KEY, true); + autoEnabledCapabilitiesPrefLoaded = true; + } + + return autoEnabledCapabilitiesSettingsPrompt; + } + set => ProjectPreferences.Set(AUTO_ENABLE_CAPABILITIES_KEY, autoEnabledCapabilitiesSettingsPrompt = value); + } + + #endregion Auto-Enable UWP Capabilities + + #region Run optimal configuration analysis on Play + + private static readonly GUIContent RunOptimalConfigContent = new GUIContent("Run optimal configuration analysis", "Run optimal configuration analysis for current project and log warnings on entering play mode or building."); + private const string RUN_OPTIMAL_CONFIG_KEY = "MixedRealityToolkit_Editor_RunOptimalConfig"; + private static bool runOptimalConfigPrefLoaded; + private static bool runOptimalConfig; + + /// + /// Should configuration analysis be run and warnings logged when settings don't match MRTK's recommendations? + /// + public static bool RunOptimalConfiguration + { + get + { + if (!runOptimalConfigPrefLoaded) + { + runOptimalConfig = ProjectPreferences.Get(RUN_OPTIMAL_CONFIG_KEY, true); + runOptimalConfigPrefLoaded = true; + } + + return runOptimalConfig; + } + set => ProjectPreferences.Set(RUN_OPTIMAL_CONFIG_KEY, runOptimalConfig = value); + } + + #endregion Run optimal configuration analysis on Play + + #region Display null data providers + + private static readonly GUIContent NullDataProviderContent = new GUIContent("Show null data providers in the profile inspector", "Mainly used for debugging unexpected behavior. Will render null data providers in red in the inspector."); + private const string NULL_DATA_PROVIDER_KEY = "MixedRealityToolkit_Editor_NullDataProviders"; + private static bool nullDataProviderPrefLoaded; + private static bool nullDataProvider; + + /// + /// Whether to show null data providers in the profile UI. + /// + /// Mainly used for debugging unexpected behavior. Data providers may be null due to a namespace change or while using an incompatible Unity version. + public static bool ShowNullDataProviders + { + get + { + if (!nullDataProviderPrefLoaded) + { + nullDataProvider = ProjectPreferences.Get(NULL_DATA_PROVIDER_KEY, false); + nullDataProviderPrefLoaded = true; + } + + return nullDataProvider; + } + set => ProjectPreferences.Set(NULL_DATA_PROVIDER_KEY, nullDataProvider = value); + } + + #endregion Display null data providers + + #region Project configuration cache + + // This section contains data that gets cached for future reference to help detect configuration + // changes that may result in a need to alert the application developer (ex: count of installed + // plugins of a specific type). There is no UI in the project settings dialog for these properties. + + private const string AUDIO_SPATIALIZER_COUNT_KEY = "MixedRealityToolkit_Editor_AudioSpatializerCount"; + private static bool audioSpatializerCountLoaded; + private static int audioSpatializerCount; + + /// + /// The cached number of audio spatializers that were most recently detected. + /// + /// Used to track when the number of installed spatializers changes. + public static int AudioSpatializerCount + { + get + { + if (!audioSpatializerCountLoaded) + { + audioSpatializerCount = ProjectPreferences.Get(AUDIO_SPATIALIZER_COUNT_KEY, 0); + audioSpatializerCountLoaded = true; + } + + return audioSpatializerCount; + } + set + { + audioSpatializerCount = value; + ProjectPreferences.Set(AUDIO_SPATIALIZER_COUNT_KEY, audioSpatializerCount); + } + } + + #endregion Project configuration cache + + [SettingsProvider] + private static SettingsProvider Preferences() + { + var provider = new SettingsProvider("Project/Mixed Reality Toolkit", SettingsScope.Project) + { + guiHandler = GUIHandler, + + keywords = new HashSet(new[] { "Mixed", "Reality", "Toolkit" }) + }; + + void GUIHandler(string searchContext) + { + EditorGUILayout.HelpBox("These settings are serialized into ProjectPreferences.asset in the MixedRealityToolkit-Generated folder.\nThis file can be checked into source control to maintain consistent settings across collaborators.", MessageType.Info); + + var prevLabelWidth = EditorGUIUtility.labelWidth; + EditorGUIUtility.labelWidth = 300f; + + bool lockProfilesResult = EditorGUILayout.Toggle(LockContent, LockProfiles); + if (lockProfilesResult != LockProfiles) + { + LockProfiles = lockProfilesResult; + } + + if (!LockProfiles) + { + EditorGUILayout.HelpBox("This is only to be used to update the default SDK profiles. If any edits are made, and not checked into the Mixed Reality Toolkit - Unity repository, the changes may be lost next time you update your local copy.", MessageType.Warning); + } + + bool ignoreResult = EditorGUILayout.Toggle(IgnoreContent, IgnoreSettingsPrompt); + if (IgnoreSettingsPrompt != ignoreResult) + { + IgnoreSettingsPrompt = ignoreResult; + } + + bool autoEnableResult = EditorGUILayout.Toggle(AutoEnableCapabilitiesContent, AutoEnableUWPCapabilities); + if (AutoEnableUWPCapabilities != autoEnableResult) + { + AutoEnableUWPCapabilities = autoEnableResult; + } + + var scriptLock = EditorGUILayout.Toggle("Is script reloading locked?", EditorAssemblyReloadManager.LockReloadAssemblies); + if (EditorAssemblyReloadManager.LockReloadAssemblies != scriptLock) + { + EditorAssemblyReloadManager.LockReloadAssemblies = scriptLock; + } + + bool optimalConfig = EditorGUILayout.Toggle(RunOptimalConfigContent, RunOptimalConfiguration); + if (RunOptimalConfiguration != optimalConfig) + { + RunOptimalConfiguration = optimalConfig; + } + + bool nullProviders = EditorGUILayout.Toggle(NullDataProviderContent, ShowNullDataProviders); + if (ShowNullDataProviders != nullProviders) + { + ShowNullDataProviders = nullProviders; + } + + EditorGUIUtility.labelWidth = prevLabelWidth; + } + + return provider; + } + } + +} diff --git a/com.microsoft.mrtk.buildwindow/Preferences/MixedRealityProjectPreferences.cs.meta b/com.microsoft.mrtk.buildwindow/Preferences/MixedRealityProjectPreferences.cs.meta new file mode 100644 index 00000000000..f80a3b31628 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Preferences/MixedRealityProjectPreferences.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 05e55ad0032547b7abc6cda4ae58de9b +timeCreated: 1663170636 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/Preferences/ProjectPreferences.cs b/com.microsoft.mrtk.buildwindow/Preferences/ProjectPreferences.cs new file mode 100644 index 00000000000..dbca801ce32 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Preferences/ProjectPreferences.cs @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using UnityEditor; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.Utilities.Editor +{ + /// + /// Utility to save preferences that should be saved per project (i.e to source control) across MRTK. Supports primitive preferences bool, int, and float + /// + public class ProjectPreferences : ScriptableObject + { + // Dictionary is not Serializable by default and furthermore System.object is not Serializable + // Thus, it is difficult to create a generic data bag. Instead we will create instances for each key preference types + [System.Serializable] + private class BoolPreferences : SerializableDictionary { } + + [System.Serializable] + private class IntPreferences : SerializableDictionary { } + + [System.Serializable] + private class FloatPreferences : SerializableDictionary { } + + [System.Serializable] + private class StringPreferences : SerializableDictionary { } + + [SerializeField] + private BoolPreferences boolPreferences = new BoolPreferences(); + + [SerializeField] + private IntPreferences intPreferences = new IntPreferences(); + + [SerializeField] + private FloatPreferences floatPreferences = new FloatPreferences(); + + [SerializeField] + private StringPreferences stringPreferences = new StringPreferences(); + + protected static string FilePath => MixedRealityToolkitFiles.MapRelativeFilePath(MODULE, DEFAULT_FILE_NAME); + + private const string DEFAULT_FILE_NAME = "ProjectPreferences.asset"; + private const MixedRealityToolkitModuleType MODULE = MixedRealityToolkitModuleType.Generated; + private static ProjectPreferences _instance; + private static ProjectPreferences Instance + { + get + { + if (_instance == null) + { + string filePath = FilePath; + if (string.IsNullOrEmpty(filePath)) + { + // MapRelativeFilePath returned null, need to build path ourselves + string modulePath = MixedRealityToolkitFiles.MapModulePath(MODULE); + if (!string.IsNullOrEmpty(modulePath)) + { + filePath = Path.Combine(modulePath, DEFAULT_FILE_NAME); + _instance = CreateInstance(); + AssetDatabase.CreateAsset(_instance, filePath); + AssetDatabase.SaveAssets(); + } + } + else + { + // Sometimes Unity has weird bug where asset file exists but Unity will not load it resulting in _instance = null. + // Force refresh of asset database before we try to access our preferences file + AssetDatabase.Refresh(); + _instance = (ProjectPreferences)AssetDatabase.LoadAssetAtPath(filePath, typeof(ProjectPreferences)); + } + } + + return _instance; + } + } + + #region Setters + + /// + /// Save bool to preferences and save to ScriptableObject with key given. + /// + /// + /// If forceSave is true (default), then will call AssetDatabase.SaveAssets which saves all assets after execution + /// + public static void Set(string key, bool value, bool forceSave = true) => Set(key, value, Instance != null ? Instance.boolPreferences : null, forceSave); + + /// + /// Save float to preferences and save to ScriptableObject with key given. + /// + /// + /// If forceSave is true (default), then will call AssetDatabase.SaveAssets which saves all assets after execution + /// + public static void Set(string key, float value, bool forceSave = true) => Set(key, value, Instance != null ? Instance.floatPreferences : null, forceSave); + + /// + /// Save int to preferences and save to ScriptableObject with key given. + /// + /// + /// If forceSave is true (default), then will call AssetDatabase.SaveAssets which saves all assets after execution + /// + public static void Set(string key, int value, bool forceSave = true) => Set(key, value, Instance != null ? Instance.intPreferences : null, forceSave); + + /// + /// Save string to preferences and save to ScriptableObject with key given. + /// + /// + /// If forceSave is true (default), then will call AssetDatabase.SaveAssets which saves all assets after execution + /// + public static void Set(string key, string value, bool forceSave = true) => Set(key, value, Instance != null ? Instance.stringPreferences : null, forceSave); + + #endregion + + #region Getters + + /// + /// Get bool from Project Preferences. If no entry found, then create new entry with provided defaultValue + /// + public static bool Get(string key, bool defaultValue) => Get(key, defaultValue, Instance != null ? Instance.boolPreferences : null); + + /// + /// Get float from Project Preferences. If no entry found, then create new entry with provided defaultValue + /// + public static float Get(string key, float defaultValue) => Get(key, defaultValue, Instance != null ? Instance.floatPreferences : null); + + /// + /// Get int from Project Preferences. If no entry found, then create new entry with provided defaultValue + /// + public static int Get(string key, int defaultValue) => Get(key, defaultValue, Instance != null ? Instance.intPreferences : null); + + /// + /// Get string from Project Preferences. If no entry found, then create new entry with provided defaultValue + /// + public static string Get(string key, string defaultValue) => Get(key, defaultValue, Instance != null ? Instance.stringPreferences : null); + + #endregion + + #region Remove + + /// + /// Remove key item from preferences if applicable + /// + public static void RemoveBool(string key) => Remove(key, Instance != null ? Instance.boolPreferences : null); + + /// + /// Remove key item from preferences if applicable + /// + public static void RemoveFloat(string key) => Remove(key, Instance != null ? Instance.floatPreferences : null); + + /// + /// Remove key item from preferences if applicable + /// + public static void RemoveInt(string key) => Remove(key, Instance != null ? Instance.intPreferences : null); + + /// + /// Remove key item from preferences if applicable + /// + public static void RemoveString(string key) => Remove(key, Instance != null ? Instance.stringPreferences : null); + + #endregion + + private static void Set(string key, T item, SerializableDictionary target, bool forceSave = true) + { + if (target == null) + { + return; + } + + if (target.ContainsKey(key)) + { + target[key] = item; + } + else + { + target.Add(key, item); + } + + if (forceSave) + { + EditorUtility.SetDirty(Instance); + AssetDatabase.SaveAssets(); + } + } + + private static T Get(string key, T defaultVal, SerializableDictionary target) + { + if (target == null) + { + return default(T); + } + + if (target.ContainsKey(key)) + { + return target[key]; + } + else + { + Set(key, defaultVal, target); + return defaultVal; + } + } + + private static bool Remove(string key, SerializableDictionary target) + { + if (target != null) + { + return target.Remove(key); + } + + return false; + } + } +} diff --git a/com.microsoft.mrtk.buildwindow/Preferences/ProjectPreferences.cs.meta b/com.microsoft.mrtk.buildwindow/Preferences/ProjectPreferences.cs.meta new file mode 100644 index 00000000000..448d92dfc52 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Preferences/ProjectPreferences.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a677e2b6d6364c818db87d3dd98e15ff +timeCreated: 1663170636 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/Preferences/SerializableDictionary.cs b/com.microsoft.mrtk.buildwindow/Preferences/SerializableDictionary.cs new file mode 100644 index 00000000000..0053be65483 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Preferences/SerializableDictionary.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.Utilities +{ + /// + /// Generic Dictionary helper class that handles serialization of keys and values into lists before/after serialization time since Dictionary by itself is not Serializable. + /// Extends C# Dictionary class to support typical API access methods + /// + /// Key type for Dictionary + /// Value type for Dictionary + [Serializable] + public class SerializableDictionary : Dictionary, ISerializationCallbackReceiver + { + [SerializeField] + private List keys = new List(); + + [SerializeField] + private List values = new List(); + + void ISerializationCallbackReceiver.OnBeforeSerialize() + { + keys.Clear(); + values.Clear(); + + foreach (KeyValuePair pair in this) + { + keys.Add(pair.Key); + values.Add(pair.Value); + } + } + + void ISerializationCallbackReceiver.OnAfterDeserialize() + { + this.Clear(); + + if (keys.Count != values.Count) + { + throw new System.Exception(string.Format($"Error after deserialization in SerializableDictionary class. There are {keys.Count} keys and {values.Count} values after deserialization. Could not load SerializableDictionary")); + } + + for (int i = 0; i < keys.Count; i++) + { + this.Add(keys[i], values[i]); + } + } + } +} diff --git a/com.microsoft.mrtk.buildwindow/Preferences/SerializableDictionary.cs.meta b/com.microsoft.mrtk.buildwindow/Preferences/SerializableDictionary.cs.meta new file mode 100644 index 00000000000..99bc6e0c9bd --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Preferences/SerializableDictionary.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f8d5e0dc433443dda3550011bccc57e5 +timeCreated: 1663170636 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/ProcessResult.cs b/com.microsoft.mrtk.buildwindow/ProcessResult.cs new file mode 100644 index 00000000000..07d57fc1963 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/ProcessResult.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.MixedReality.Toolkit.Utilities +{ + /// + /// Result from a completed asynchronous process. + /// + public struct ProcessResult + { + /// + /// Exit code from completed process. + /// + public int ExitCode { get; } + + /// + /// Errors from completed process. + /// + public string[] Errors { get; } + + /// + /// Output from completed process. + /// + public string[] Output { get; } + + /// + /// Constructor for Process Result. + /// + /// Exit code from completed process. + /// Errors from completed process. + /// Output from completed process. + public ProcessResult(int exitCode, string[] errors, string[] output) : this() + { + ExitCode = exitCode; + Errors = errors; + Output = output; + } + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/ProcessResult.cs.meta b/com.microsoft.mrtk.buildwindow/ProcessResult.cs.meta new file mode 100644 index 00000000000..719746c38a6 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/ProcessResult.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3c7b02ae399a48fca53c0402233f0aa4 +timeCreated: 1663171251 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/Setup.meta b/com.microsoft.mrtk.buildwindow/Setup.meta new file mode 100644 index 00000000000..b21e730f377 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Setup.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4bacf0fc6a824830a9bed3e613dba506 +timeCreated: 1663171429 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/Setup/MixedRealityToolkitFiles.cs b/com.microsoft.mrtk.buildwindow/Setup/MixedRealityToolkitFiles.cs new file mode 100644 index 00000000000..e192dce4fe2 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Setup/MixedRealityToolkitFiles.cs @@ -0,0 +1,598 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.Utilities.Editor +{ + /// + /// Base folder types for modules searched by the MixedRealityToolkitFiles utility. + /// + public enum MixedRealityToolkitModuleType + { + None = 0, + Core, + Generated, + Providers, + Services, + SDK, + Examples, + Tests, + Extensions, + Tools, + StandardAssets, + // This module only exists for testing purposes, and is used in edit mode tests in conjunction + // with MixedRealityToolkitFiles to ensure that this class is able to reason over MRTK + // files that are placed outside of the root asset folder. + AdhocTesting = -1, + } + + /// + /// API for working with MixedRealityToolkit folders contained in the project. + /// + /// + /// This class works by looking for sentinel files (following the pattern MRTK.*.sentinel, + /// for example, MRTK.Core.sentinel) in order to identify where the MRTK is located + /// within the project. + /// + public static class MixedRealityToolkitFiles + { + /// + /// This controls the behavior of MapRelativePathToAbsolutePath. + /// + private enum SearchType + { + /// + /// Search for a file. + /// + File, + /// + /// Search for a folder. + /// + Folder, + } + + /// + /// The MRTK uses "sentinel" files (for example, MRTK.Core.sentinel) which are used to uniquely + /// identify the presence of certain MRTK folders and modules. This is the file pattern used + /// to search within folders for those sentinel files and make the file search a little more + /// efficient than a full file enumeration. + /// + private const string SentinelFilePattern = "MRTK.*.sentinel"; + + /// + /// In order to subscribe for a callback, + /// the class declaring the method must derive from AssetPostprocessor. So this class is nested privately as to prevent instantiation of it. + /// + private class AssetPostprocessor : UnityEditor.AssetPostprocessor + { + public static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) + { + foreach (string asset in importedAssets.Concat(movedAssets)) + { + if (IsSentinelFile(asset)) + { + string fullAssetPath = ResolveFullAssetsPath(asset); + TryRegisterModuleViaFile(fullAssetPath); + } + } + + foreach (string asset in deletedAssets.Concat(movedFromAssetPaths)) + { + if (IsSentinelFile(asset)) + { + string fullAssetPath = ResolveFullAssetsPath(asset); + string folderPath = Path.GetDirectoryName(fullAssetPath); + TryUnregisterModuleFolder(folderPath); + } + } + } + } + + // Storage of our list of module paths (stored as absolute file paths) and bucketed by ModuleType + private readonly static Dictionary> mrtkFolders = + new Dictionary>(); + + private static Task searchForFoldersTask = null; + private static CancellationTokenSource searchForFoldersToken; + + // This ensures directory separator chars are platform independent. Given path might use \ or / + // Should use string.NormalizeSeparators() extension but blocked by #7152 + private static string NormalizeSeparators(string path) => + path?.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); + + private static string FormatSeparatorsForUnity(string path) => path?.Replace('\\', '/'); + + private static bool isInitialized = false; + + private static readonly Dictionary moduleNameMap = new Dictionary() + { + { "Core", MixedRealityToolkitModuleType.Core }, + { "Generated", MixedRealityToolkitModuleType.Generated }, + { "Providers", MixedRealityToolkitModuleType.Providers }, + { "Services", MixedRealityToolkitModuleType.Services }, + { "SDK", MixedRealityToolkitModuleType.SDK }, + { "Examples", MixedRealityToolkitModuleType.Examples }, + { "Tests", MixedRealityToolkitModuleType.Tests }, + { "Extensions", MixedRealityToolkitModuleType.Extensions }, + { "Tools", MixedRealityToolkitModuleType.Tools }, + { "StandardAssets", MixedRealityToolkitModuleType.StandardAssets }, + + // This module only exists for testing purposes, and is used in edit mode tests in conjunction + // with MixedRealityToolkitFiles to ensure that this class is able to reason over MRTK + // files that are placed outside of the root asset folder. + { "AdhocTesting", MixedRealityToolkitModuleType.AdhocTesting }, + }; + + /// + /// Maps an absolute path to be relative to the Project Root path (the Unity folder that contains Assets) + /// + /// The absolute path to the project. + /// The project relative path. + /// This doesn't produce paths that contain step out '..' relative paths. + public static string GetAssetDatabasePath(string absolutePath) + { + string assetDatabasePath = Path.GetFullPath(absolutePath).Replace("\\", "/"); + string token = string.Empty; + string newRoot = string.Empty; + if (assetDatabasePath.Contains("/Assets/")) + { + token = "/Assets/"; + newRoot = "Assets"; + } + else if (assetDatabasePath.Contains("/PackageCache/")) + { + token = "/PackageCache/"; + newRoot = "Packages"; + + // PackageCache folders need the embedded version removed. + int atIndex = assetDatabasePath.IndexOf("@"); + int separatorIndex = assetDatabasePath.Substring(atIndex).IndexOf("/"); + string versionString = assetDatabasePath.Substring(atIndex, separatorIndex); + assetDatabasePath = assetDatabasePath.Replace(versionString, ""); + } + else if (assetDatabasePath.Contains("/Packages/")) + { + token = "/Packages/"; + newRoot = "Packages"; + } + + if (!string.IsNullOrWhiteSpace(newRoot) && + !string.IsNullOrWhiteSpace(token)) + { + string oldRoot = assetDatabasePath.Substring(0, + assetDatabasePath.LastIndexOf(token) + token.Length - 1); // Subtract 1 to keep the trailing slash + assetDatabasePath = assetDatabasePath.Replace(oldRoot, newRoot); + } + return assetDatabasePath; + } + + /// + /// Returns a collection of MRTK Core directories found in the project. + /// + /// + /// File/Folder paths returned are absolute, not relative + /// + public static IEnumerable MRTKDirectories => GetDirectories(MixedRealityToolkitModuleType.Core); + + /// + /// Get list of discovered directories for provided module type + /// + /// Module type to filter against + /// string list of discovered directory paths + /// + /// File/Folder paths returned are absolute, not relative + /// + public static IEnumerable GetDirectories(MixedRealityToolkitModuleType module) + { + if (mrtkFolders.TryGetValue(module, out HashSet folders)) + { + return folders; + } + return null; + } + + /// + /// Are any of the MRTK directories available? + /// + /// + /// If a search is currently in progress, then property will wait synchronously for the task to finish with timeout of 1 second + /// + public static bool AreFoldersAvailable + { + get + { + // Other components that InitializeOnLoad may be called before our static constructor. If that is the case, initialize now + if (!isInitialized) + { + Init(); + } + + // If we are currently searching for folders, wait up to 1 second for operation to complete + if (searchForFoldersTask != null) + { + searchForFoldersTask.Wait(1000); + } + + return mrtkFolders.Count > 0; + } + } + + private static void Init() + { + // Note that this file used to have an InitializeOnLoad handler to handle + // early initialization of the folder refresh. However, this had an effect of slowing down + // the Unity editor (i.e. on play mode entry, on recompile) even in cases where the MRTK + // isn't in the scene. + if (!isInitialized) + { + RefreshFolders(); + } + + isInitialized = true; + } + + /// + /// Force refresh of MRTK tracked folders. Fires and forgets async call. Returns immediately + /// + /// + /// Kicks off async refresh of the MRTK folder database. + /// + public static void RefreshFolders() + { + // MRTK may be located in Assets (.unitypackage import) or the Packages (UPM import) + // folder. Check both locations. + List rootFolders = new List + { + Application.dataPath, + Path.GetFullPath("Packages"), + Path.GetFullPath(Path.Combine("Library", "PackageCache")) + }; + searchForFoldersTask = Task.Run(() => SearchForFoldersAsync(rootFolders)); + } + + /// + /// Get task tracking folder refresh if component wants to wait for files to be ready + /// + public static async Task WaitForFolderRefresh() + { + await searchForFoldersTask; + } + + /// + /// Returns files from all folder instances of the core MRTK folder relative path. + /// + /// The core MRTK folder relative path to the target folder. + /// The array of files. + public static string[] GetFiles(string mrtkRelativeFolder) + { + return GetFiles(MixedRealityToolkitModuleType.Core, mrtkRelativeFolder); + } + + /// + /// Returns files from all folder instances of the MRTK folder relative path. + /// + /// The MRTK folder relative path to the target folder. + /// The array of files. + public static string[] GetFiles(MixedRealityToolkitModuleType module, string mrtkRelativeFolder) + { + if (!AreFoldersAvailable) + { + Debug.LogWarning("Failed to locate MixedRealityToolkit folders in the project."); + return null; + } + + if (mrtkFolders.TryGetValue(module, out HashSet modFolders)) + { + return modFolders + .Select(t => Path.Combine(t, mrtkRelativeFolder)) + .Where(Directory.Exists) + .SelectMany(t => Directory.GetFiles(t)) + .Select(GetAssetDatabasePath) + .ToArray(); + } + return null; + } + + /// + /// Maps a single relative path file to a concrete path from one of the core MRTK folders, if found. Otherwise returns null. + /// + /// The core MRTK folder relative path to the file. + /// The project relative path to the file. + public static string MapRelativeFilePath(string mrtkPathToFile) + { + return MapRelativeFilePath(MixedRealityToolkitModuleType.Core, mrtkPathToFile); + } + + /// + /// Maps a single relative path file to a concrete path from one of the MRTK folders, if found. Otherwise returns null. + /// + /// The MRTK folder relative path to the file. + /// The project relative path to the file. + public static string MapRelativeFilePath(MixedRealityToolkitModuleType module, string mrtkPathToFile) + { + string absolutePath = MapRelativeFilePathToAbsolutePath(module, mrtkPathToFile); + return absolutePath != null ? GetAssetDatabasePath(absolutePath) : null; + } + + /// + /// Maps a single relative path file to MRTK folders to its absolute path, if found. Otherwise returns null. + /// + /// + /// For example, this will map "Inspectors\Data\EditorWindowOptions.json" to its full path like + /// "c:\project\Assets\Libs\MRTK\MixedRealityToolkit\Inspectors\Data\EditorWindowOptions.json". + /// This assumes that the passed in mrtkPathToFile is found under the "MixedRealityToolkit" folder + /// (instead of the MixedRealityToolkit.SDK, or any of the other folders). + /// + public static string MapRelativeFilePathToAbsolutePath(string mrtkPathToFile) + { + return MapRelativeFilePathToAbsolutePath(MixedRealityToolkitModuleType.Core, mrtkPathToFile); + } + + /// + /// Overload of MapRelativeFilePathToAbsolutePath which provides the ability to specify the module that the + /// file belongs to. + /// + /// + /// When searching for a resource that lives in the MixedRealityToolkit.SDK folder, this could be invoked + /// in this way: + /// MapRelativeFilePathToAbsolutePath(MixedRealityToolkitModuleType.SDK, mrtkPathToFile) + /// + public static string MapRelativeFilePathToAbsolutePath(MixedRealityToolkitModuleType module, string mrtkPathToFile) + { + return MapRelativePathToAbsolutePath(SearchType.File, module, mrtkPathToFile); + } + + /// + /// Similar to MapRelativeFilePathToAbsolutePath, except this checks for the existence of a folder instead of file. + /// + /// + /// Returns first valid path found + /// + public static string MapRelativeFolderPathToAbsolutePath(MixedRealityToolkitModuleType module, string mrtkPathToFolder) + { + return MapRelativePathToAbsolutePath(SearchType.Folder, module, mrtkPathToFolder); + } + + /// + /// Get the relative asset folder path to the provided Module type + /// + /// Module type to search for + /// + /// Returns first valid module folder path (relative) found. Returns null otherwise. + /// + public static string MapModulePath(MixedRealityToolkitModuleType module) + { + var path = MapRelativeFolderPathToAbsolutePath(module, ""); + return path != null ? GetAssetDatabasePath(path) : null; + } + + /// + /// Finds the module type, if found, from the specified package folder name. + /// + /// The asset folder name (ex: MixedRealityToolkit.Providers) + /// + /// associated with the package folder name. Returns + /// MixedRealityToolkitModuleType.None if an appropriate module type could not be found. + /// + public static MixedRealityToolkitModuleType GetModuleFromPackageFolder(string packageFolder) + { + if (!packageFolder.StartsWith("MixedRealityToolkit")) + { + // There are no mappings for folders that do not start with "MixedRealityToolkit" + return MixedRealityToolkitModuleType.None; + } + + int separatorIndex = packageFolder.IndexOf('.'); + packageFolder = (separatorIndex != -1) ? packageFolder.Substring(separatorIndex + 1) : "Core"; + + MixedRealityToolkitModuleType moduleType; + return moduleNameMap.TryGetValue(packageFolder, out moduleType) ? moduleType : MixedRealityToolkitModuleType.None; + } + + /// + /// Creates the MixedRealityToolkit.Generated folder if it does not exist and returns the + /// path to the generated folder. + /// + public static string GetGeneratedFolder + { + get + { + TryToCreateGeneratedFolder(); + return MapModulePath(MixedRealityToolkitModuleType.Generated); + } + } + + private static async Task SearchForFoldersAsync(List rootFolders) + { + if (searchForFoldersToken != null) + { + searchForFoldersToken.Cancel(); + } + + searchForFoldersToken = new CancellationTokenSource(); + await Task.Run(() => + { + for (int i = 0; i < rootFolders.Count; i++) + { + SearchForFolders(rootFolders[i], searchForFoldersToken.Token); + } + }, searchForFoldersToken.Token); + searchForFoldersToken = null; + } + + private static void SearchForFolders(string rootPath, CancellationToken ct) + { + try + { + var filePathResults = Directory.GetFiles(rootPath, SentinelFilePattern, SearchOption.AllDirectories); + foreach (var sentinelFilePath in filePathResults) + { + TryRegisterModuleViaFile(sentinelFilePath); + + if (ct.IsCancellationRequested) + { + ct.ThrowIfCancellationRequested(); + } + } + + // Create the Generated folder, if the user tries to delete the Generated folder it will be created again + TryToCreateGeneratedFolder(); + } + catch (OperationCanceledException) + { + Console.WriteLine($"\n{nameof(OperationCanceledException)} thrown\n"); + } + catch (Exception ex) + { + Debug.LogError(ex.Message); + } + } + + private static void TryRegisterModuleViaFile(string filePath) + { + MixedRealityToolkitModuleType moduleType = GetModuleType(filePath); + if (moduleType != MixedRealityToolkitModuleType.None) + { + string folderPath = Path.GetDirectoryName(filePath); + + RegisterFolderToModule(folderPath, moduleType); + } + } + + private static void RegisterFolderToModule(string folderPath, MixedRealityToolkitModuleType module) + { + string normalizedFolder = NormalizeSeparators(folderPath); + if (!mrtkFolders.TryGetValue(module, out HashSet modFolders)) + { + modFolders = new HashSet(); + mrtkFolders.Add(module, modFolders); + } + + modFolders.Add(normalizedFolder); + } + + private static void TryToCreateGeneratedFolder() + { + // Always add the MixedRealityToolkit.Generated folder to Assets + var generatedDirs = GetDirectories(MixedRealityToolkitModuleType.Generated); + if (generatedDirs == null || !generatedDirs.Any()) + { + string generatedFolderPath = Path.Combine("Assets", "MixedRealityToolkit.Generated"); + if (!Directory.Exists(generatedFolderPath)) + { + Directory.CreateDirectory(generatedFolderPath); + } + + string generatedSentinelFilePath = Path.Combine(generatedFolderPath, "MRTK.Generated.sentinel"); + if (!File.Exists(generatedSentinelFilePath)) + { + // Make sure we create and dispose/close the filestream just created + using (var f = File.Create(generatedSentinelFilePath)) { } + } + + TryRegisterModuleViaFile(generatedSentinelFilePath); + } + } + + private static bool TryUnregisterModuleFolder(string folderPath) + { + string normalizedFolder = NormalizeSeparators(folderPath); + bool found = false; + var removeKeys = new HashSet(); + foreach (var modFolders in mrtkFolders) + { + if (modFolders.Value.Remove(normalizedFolder)) + { + if (modFolders.Value.Count == 0) + { + removeKeys.Add(modFolders.Key); + } + found = true; + } + } + + foreach (var key in removeKeys) + { + mrtkFolders.Remove(key); + } + + return found; + } + + /// + /// Maps a single relative path (file or folder) in MRTK folders to its absolute path, if found. + /// Otherwise returns null. + /// + private static string MapRelativePathToAbsolutePath(SearchType searchType, MixedRealityToolkitModuleType module, string mrtkPath) + { + if (!AreFoldersAvailable) + { + Debug.LogWarning("Failed to locate MixedRealityToolkit folders in the project."); + return null; + } + + mrtkPath = NormalizeSeparators(mrtkPath); + + if (mrtkFolders.TryGetValue(module, out HashSet modFolders)) + { + string path = modFolders + .Select(t => Path.Combine(t, mrtkPath)) + .FirstOrDefault(t => searchType == SearchType.File ? File.Exists(t) : Directory.Exists(t)); + + return path; + } + + return null; + } + + /// + /// Given the full file path, returns the module it's associated with (if it is an MRTK sentinel file) + /// + private static MixedRealityToolkitModuleType GetModuleType(string filePath) + { + const string sentinelRegexPattern = @"^MRTK\.(?[a-zA-Z]+)\.sentinel"; + string fileName = Path.GetFileName(filePath); + var matches = Regex.Matches(fileName, sentinelRegexPattern); + if (matches.Count == 1) + { + var moduleName = matches[0].Groups["module"].Value; + MixedRealityToolkitModuleType moduleType; + if (moduleNameMap.TryGetValue(moduleName, out moduleType)) + { + return moduleType; + } + } + return MixedRealityToolkitModuleType.None; + } + + private static bool IsSentinelFile(string assetPath) + { + return Regex.IsMatch(Path.GetFileName(assetPath), SentinelFilePattern); + } + + /// + /// Resolves the given asset to its full path if and only if the asset belongs to the + /// Assets folder (i.e. it is prefixed with "Assets/..." + /// + /// + /// If not associated with the Assets folder, will return the path unchanged. + /// + private static string ResolveFullAssetsPath(string path) + { + if (path.StartsWith("Assets")) + { + // asset.Substring(6) represents the characters after the "Assets" string. + return Application.dataPath + path.Substring(6); + } + return path; + } + } +} diff --git a/com.microsoft.mrtk.buildwindow/Setup/MixedRealityToolkitFiles.cs.meta b/com.microsoft.mrtk.buildwindow/Setup/MixedRealityToolkitFiles.cs.meta new file mode 100644 index 00000000000..476df9941e2 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Setup/MixedRealityToolkitFiles.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3e7f9e894ac244a1aa98ea70e3a90255 +timeCreated: 1663171429 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/Setup/MixedRealityToolkitPreserveSettings.cs b/com.microsoft.mrtk.buildwindow/Setup/MixedRealityToolkitPreserveSettings.cs new file mode 100644 index 00000000000..979909bde22 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Setup/MixedRealityToolkitPreserveSettings.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using System.Xml; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.Utilities.Editor +{ + /// + /// Manages the Mixed Reality Toolkit code preservation settings. Please see + /// https://docs.unity3d.com/Manual/ManagedCodeStripping.html for more information. + /// + internal static class MixedRealityToolkitPreserveSettings + { + /// + /// The data that will be written to the link.xml file if this class creates one on + /// behalf of the project. + /// + private const string defaultLinkXmlContents = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + /// + /// Ensure that a link.xml file exists in the MixedRealityToolkit.Generated folder. + /// This file is used to control the Unity linker's byte code stripping of MRTK assemblies. + /// + public static void EnsureLinkXml() + { + string generatedFolder = MixedRealityToolkitFiles.MapRelativeFolderPathToAbsolutePath(MixedRealityToolkitModuleType.Generated, ""); + string linkXmlPath = Path.Combine(generatedFolder, "link.xml"); + + if (File.Exists(linkXmlPath)) + { + bool xmlUpdated = false; + + // Update to ensure Unity's okay ignoring missing assemblies + XmlDocument doc = new XmlDocument(); + doc.Load(linkXmlPath); + + XmlNodeList assemblyNodes = doc.SelectNodes(@"/linker/assembly"); + + if (assemblyNodes != null) + { + XmlAttribute newAttr = doc.CreateAttribute("ignoreIfMissing"); + newAttr.Value = "1"; + + foreach (XmlNode assembly in assemblyNodes) + { + XmlNode fullname = assembly.Attributes.GetNamedItem("fullname"); + XmlNode ignoreIfMissing = assembly.Attributes.GetNamedItem("ignoreIfMissing"); + if (ignoreIfMissing == null && fullname.InnerText.StartsWith("Microsoft.MixedReality.Toolkit")) + { + assembly.Attributes.SetNamedItem(newAttr); + xmlUpdated = true; + } + } + } + + if (xmlUpdated) + { + Debug.Log($"The link.xml file in {MixedRealityToolkitFiles.GetGeneratedFolder} was updated to include \"ignoreIfMissing\" tags.\n" + + "This is required in Unity 2021 or later for legacy XR assemblies that are no longer used and is okay if this project is on an earlier version."); + doc.Save(linkXmlPath); + } + + return; + } + + // Create a default link.xml with an initial set of assembly preservation rules. + using (StreamWriter writer = new StreamWriter(linkXmlPath)) + { + writer.WriteLine(defaultLinkXmlContents); + Debug.Log($"A link.xml file was created in {MixedRealityToolkitFiles.GetGeneratedFolder}.\n" + + "This file is used to control preservation of MRTK code during linking. It is recommended to add link.xml (and link.xml.meta) to source control."); + } + } + } +} diff --git a/com.microsoft.mrtk.buildwindow/Setup/MixedRealityToolkitPreserveSettings.cs.meta b/com.microsoft.mrtk.buildwindow/Setup/MixedRealityToolkitPreserveSettings.cs.meta new file mode 100644 index 00000000000..db31217ffb0 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/Setup/MixedRealityToolkitPreserveSettings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: eff449bdc0184fce9f08c64d43ece7cb +timeCreated: 1663171429 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/SyncContextUtility.cs b/com.microsoft.mrtk.buildwindow/SyncContextUtility.cs new file mode 100644 index 00000000000..ef7c1b2ef4c --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/SyncContextUtility.cs @@ -0,0 +1,83 @@ +// MIT License + +// Copyright(c) 2016 Modest Tree Media Inc + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Threading; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.Utilities +{ + /// + /// Utility class to assist in thread and context synchronization. + /// + public static class SyncContextUtility + { +#if UNITY_EDITOR + private static System.Reflection.MethodInfo executionMethod; + + /// + /// HACK: makes Unity Editor execute continuations in edit mode. + /// + private static void ExecuteContinuations() + { + if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) + { + return; + } + + var context = SynchronizationContext.Current; + + if (executionMethod == null) + { + executionMethod = context.GetType().GetMethod("Exec", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + } + + executionMethod?.Invoke(context, null); + } + + [UnityEditor.InitializeOnLoadMethod] +#endif + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void Initialize() + { +#if UNITY_EDITOR + UnityEditor.EditorApplication.update += ExecuteContinuations; +#endif + UnitySynchronizationContext = SynchronizationContext.Current; + UnityThreadId = Thread.CurrentThread.ManagedThreadId; + } + + /// + /// This Unity Player's Thread Id. + /// + public static int UnityThreadId { get; private set; } + + /// + /// This Unity Player's Synchronization Context. + /// + public static SynchronizationContext UnitySynchronizationContext { get; private set; } + + /// + /// Is this being called from the main thread? + /// + public static bool IsMainThread => UnitySynchronizationContext == SynchronizationContext.Current; + } +} diff --git a/com.microsoft.mrtk.buildwindow/SyncContextUtility.cs.meta b/com.microsoft.mrtk.buildwindow/SyncContextUtility.cs.meta new file mode 100644 index 00000000000..159a512b489 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/SyncContextUtility.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3459a54609d949b0917eebfdcec2a7c8 +timeCreated: 1663172217 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/USB.meta b/com.microsoft.mrtk.buildwindow/USB.meta new file mode 100644 index 00000000000..ec26002d50c --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/USB.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d4cd4d2b8edb41f9a9be20fe200ebb48 +timeCreated: 1663169404 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/USB/USBDeviceInfo.cs b/com.microsoft.mrtk.buildwindow/USB/USBDeviceInfo.cs new file mode 100644 index 00000000000..3cde444c9a6 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/USB/USBDeviceInfo.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.Utilities.Editor +{ + [Serializable] + public class USBDeviceInfo + { + public USBDeviceInfo(int vendorId, string udid, int productId, string name, int revision) + { + VendorId = vendorId; + Udid = udid; + ProductId = productId; + Name = name; + Revision = revision; + } + + public int VendorId { get; private set; } + + public string Udid { get; private set; } + + public int ProductId { get; private set; } + + public string Name { get; private set; } + + public int Revision { get; private set; } + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/USB/USBDeviceInfo.cs.meta b/com.microsoft.mrtk.buildwindow/USB/USBDeviceInfo.cs.meta new file mode 100644 index 00000000000..e644244ce88 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/USB/USBDeviceInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3bea91eac1664dbf9ce93192f0977ee7 +timeCreated: 1663169404 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/USB/USBDeviceListener.cs b/com.microsoft.mrtk.buildwindow/USB/USBDeviceListener.cs new file mode 100644 index 00000000000..f82d2d6bf4e --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/USB/USBDeviceListener.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using UnityEditor; +using UnityEditor.Hardware; + +namespace Microsoft.MixedReality.Toolkit.Utilities.Editor +{ + [InitializeOnLoad] + public class USBDeviceListener + { + public static USBDeviceInfo[] USBDevices; + + public delegate void OnUsbDevicesChanged(UsbDevice[] usbDevices); + + public static event OnUsbDevicesChanged UsbDevicesChanged; + + private static readonly List USBDevicesList = new List(0); + + static USBDeviceListener() + { + UnityEditor.Hardware.Usb.DevicesChanged += NotifyUsbDevicesChanged; + } + + private static void NotifyUsbDevicesChanged(UsbDevice[] devices) + { + UsbDevicesChanged?.Invoke(devices); + + USBDevicesList.Clear(); + + foreach (UsbDevice device in devices) + { + USBDevicesList.Add(new USBDeviceInfo(device.vendorId, device.udid, device.productId, device.name, device.revision)); + } + + USBDevices = USBDevicesList.ToArray(); + } + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/USB/USBDeviceListener.cs.meta b/com.microsoft.mrtk.buildwindow/USB/USBDeviceListener.cs.meta new file mode 100644 index 00000000000..c009b0386f0 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/USB/USBDeviceListener.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 50baa8d5cf5b40759f0e26c43d6922eb +timeCreated: 1663169404 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WaitForUpdate.cs b/com.microsoft.mrtk.buildwindow/WaitForUpdate.cs new file mode 100644 index 00000000000..b4ac348c1fb --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WaitForUpdate.cs @@ -0,0 +1,35 @@ +// MIT License + +// Copyright(c) 2016 Modest Tree Media Inc + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.Utilities +{ + /// + /// This can be used as a way to return to the main unity thread + /// when using multiple threads with async methods. + /// + public class WaitForUpdate : CustomYieldInstruction + { + public override bool keepWaiting => false; + } +} diff --git a/com.microsoft.mrtk.buildwindow/WaitForUpdate.cs.meta b/com.microsoft.mrtk.buildwindow/WaitForUpdate.cs.meta new file mode 100644 index 00000000000..d24ac4c1341 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WaitForUpdate.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b07f823c48b44a768461de28c2b205cf +timeCreated: 1663172267 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WebRequestRest.meta b/com.microsoft.mrtk.buildwindow/WebRequestRest.meta new file mode 100644 index 00000000000..797dcd24ac4 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WebRequestRest.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9eb67d04954c46038bebf7c8feeafec7 +timeCreated: 1663170292 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WebRequestRest/Response.cs b/com.microsoft.mrtk.buildwindow/WebRequestRest/Response.cs new file mode 100644 index 00000000000..63e18110cab --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WebRequestRest/Response.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.MixedReality.Toolkit.Utilities +{ + /// + /// Response to a REST Call. + /// + public struct Response + { + /// + /// Was the REST call successful? + /// + public bool Successful { get; } + + /// + /// Response body from the resource. + /// + public string ResponseBody => responseBody ?? (responseBody = responseBodyAction?.Invoke()); + + /// + /// Response body from the resource. + /// + public async Task GetResponseBody() + { + if (responseBody != null) + { + return responseBody; + } + return await responseBodyTask; + } + + private string responseBody; + private Func responseBodyAction; + private Task responseBodyTask; + + /// + /// Response data from the resource. + /// + public byte[] ResponseData => responseData ?? (responseData = responseDataAction?.Invoke()); + private byte[] responseData; + private Func responseDataAction; + + /// + /// Response code from the resource. + /// + public long ResponseCode { get; } + + /// + /// Constructor. + /// + public Response(bool successful, string responseBody, byte[] responseData, long responseCode) + { + Successful = successful; + responseBodyAction = null; + responseBodyTask = null; + this.responseBody = responseBody; + responseDataAction = null; + this.responseData = responseData; + ResponseCode = responseCode; + } + + public Response(bool successful, Func responseBodyAction, Func responseDataAction, long responseCode) + { + Successful = successful; + this.responseBodyAction = responseBodyAction; + responseBodyTask = ResponseUtils.BytesToString(responseDataAction.Invoke()); + responseBody = null; + this.responseDataAction = responseDataAction; + responseData = null; + ResponseCode = responseCode; + } + + public Response(bool successful, Task responseBodyTask, Func responseDataAction, long responseCode) + { + Successful = successful; + responseBodyAction = () => System.Text.Encoding.Default.GetString(responseDataAction.Invoke()); + this.responseBodyTask = responseBodyTask; + responseBody = null; + this.responseDataAction = responseDataAction; + responseData = null; + ResponseCode = responseCode; + } + + public Response(bool successful, Func responseDataAction, long responseCode) + { + Successful = successful; + responseBodyAction = () => System.Text.Encoding.Default.GetString(responseDataAction.Invoke()); + responseBodyTask = ResponseUtils.BytesToString(responseDataAction.Invoke()); + responseBody = null; + this.responseDataAction = responseDataAction; + responseData = null; + ResponseCode = responseCode; + } + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WebRequestRest/Response.cs.meta b/com.microsoft.mrtk.buildwindow/WebRequestRest/Response.cs.meta new file mode 100644 index 00000000000..e02de11b44c --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WebRequestRest/Response.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bae8eee0d1f64494baadaf8c9c0efdc6 +timeCreated: 1663170292 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WebRequestRest/ResponseUtils.cs b/com.microsoft.mrtk.buildwindow/WebRequestRest/ResponseUtils.cs new file mode 100644 index 00000000000..bb3e14f0b91 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WebRequestRest/ResponseUtils.cs @@ -0,0 +1,14 @@ +using System; +using System.Threading.Tasks; + +namespace Microsoft.MixedReality.Toolkit.Utilities +{ + public struct ResponseUtils + { + /// + /// Static Func for create convert Task + /// + public static Func> BytesToString = async (byteArray) => await Task.Run(() => + System.Text.Encoding.Default.GetString(byteArray)).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WebRequestRest/ResponseUtils.cs.meta b/com.microsoft.mrtk.buildwindow/WebRequestRest/ResponseUtils.cs.meta new file mode 100644 index 00000000000..de08c106849 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WebRequestRest/ResponseUtils.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1289e6ffece14b839a70f93616e09b38 +timeCreated: 1663170292 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WebRequestRest/Rest.cs b/com.microsoft.mrtk.buildwindow/WebRequestRest/Rest.cs new file mode 100644 index 00000000000..d657e2eb016 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WebRequestRest/Rest.cs @@ -0,0 +1,384 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.Networking; + +namespace Microsoft.MixedReality.Toolkit.Utilities +{ + + /// + /// REST Class for CRUD Transactions. + /// + public static class Rest + { + #region Authentication + + /// + /// Gets the Basic auth header. + /// + /// The Username. + /// The password. + /// The Basic authorization header encoded to base 64. + public static string GetBasicAuthentication(string username, string password) + { + return $"Basic {Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes($"{username}:{password}"))}"; + } + + /// + /// Gets the Bearer auth header. + /// + /// OAuth Token to be used. + /// The Bearer authorization header. + public static string GetBearerOAuthToken(string authToken) + { + return $"Bearer {authToken}"; + } + + #endregion Authentication + + #region GET + + /// + /// Rest GET. + /// + /// Finalized Endpoint Query with parameters. + /// Optional header information for the request. + /// Optional time in seconds before request expires. + /// Optional DownloadHandler for the request. + /// Optional bool. If its true, response data will be read from web request download handler. + /// Optional certificate handler for custom certificate verification + /// Optional bool. If true and is not null, will be disposed, when the underlying UnityWebRequest is disposed. + /// The response data. + public static async Task GetAsync( + string query, + Dictionary headers = null, + int timeout = -1, + DownloadHandler downloadHandler = null, + bool readResponseData = false, + CertificateHandler certificateHandler = null, + bool disposeCertificateHandlerOnDispose = true, + CancellationToken cancellationToken = default(CancellationToken)) + { + using (var webRequest = UnityWebRequest.Get(query)) + { + if (downloadHandler != null) + { + webRequest.downloadHandler = downloadHandler; + } + cancellationToken.Register(() => + { + webRequest.Abort(); + }); + + return await ProcessRequestAsync(webRequest, timeout, headers, readResponseData, certificateHandler, disposeCertificateHandlerOnDispose); + } + } + + #endregion GET + + #region POST + + /// + /// Rest POST. + /// + /// Finalized Endpoint Query with parameters. + /// Optional header information for the request. + /// Optional time in seconds before request expires. + /// Optional bool. If its true, response data will be read from web request download handler. + /// Optional certificate handler for custom certificate verification + /// Optional bool. If true and is not null, will be disposed, when the underlying UnityWebRequest is disposed. + /// The response data. + public static async Task PostAsync( + string query, + Dictionary headers = null, + int timeout = -1, + bool readResponseData = false, + CertificateHandler certificateHandler = null, + bool disposeCertificateHandlerOnDispose = true, + CancellationToken cancellationToken = default(CancellationToken)) + { + using (var webRequest = UnityWebRequest.Post(query, null as string)) + { + cancellationToken.Register(() => + { + webRequest.Abort(); + }); + return await ProcessRequestAsync(webRequest, timeout, headers, readResponseData, certificateHandler, disposeCertificateHandlerOnDispose); + } + } + + /// + /// Rest POST. + /// + /// Finalized Endpoint Query with parameters. + /// Form Data. + /// Optional header information for the request. + /// Optional time in seconds before request expires. + /// Optional bool. If its true, response data will be read from web request download handler. + /// Optional certificate handler for custom certificate verification + /// Optional bool. If true and is not null, will be disposed, when the underlying UnityWebRequest is disposed. + /// The response data. + public static async Task PostAsync( + string query, + WWWForm formData, + Dictionary headers = null, + int timeout = -1, bool readResponseData = false, + CertificateHandler certificateHandler = null, + bool disposeCertificateHandlerOnDispose = true, + CancellationToken cancellationToken = default(CancellationToken)) + { + using (var webRequest = UnityWebRequest.Post(query, formData)) + { + cancellationToken.Register(() => + { + webRequest.Abort(); + }); + return await ProcessRequestAsync(webRequest, timeout, headers, readResponseData, certificateHandler, disposeCertificateHandlerOnDispose); + } + } + + /// + /// Rest POST. + /// + /// Finalized Endpoint Query with parameters. + /// JSON data for the request. + /// Optional header information for the request. + /// Optional time in seconds before request expires. + /// Optional bool. If its true, response data will be read from web request download handler. + /// Optional certificate handler for custom certificate verification + /// Optional bool. If true and is not null, will be disposed, when the underlying UnityWebRequest is disposed. + /// The response data. + public static async Task PostAsync( + string query, + string jsonData, + Dictionary headers = null, + int timeout = -1, + bool readResponseData = false, + CertificateHandler certificateHandler = null, + bool disposeCertificateHandlerOnDispose = true, + CancellationToken cancellationToken = default(CancellationToken)) + { + using (var webRequest = UnityWebRequest.Post(query, "POST")) + { + cancellationToken.Register(() => + { + webRequest.Abort(); + }); + var data = new UTF8Encoding().GetBytes(jsonData); + webRequest.uploadHandler = new UploadHandlerRaw(data); + webRequest.downloadHandler = new DownloadHandlerBuffer(); + webRequest.SetRequestHeader("Content-Type", "application/json"); + webRequest.SetRequestHeader("Accept", "application/json"); + return await ProcessRequestAsync(webRequest, timeout, headers, readResponseData, certificateHandler, disposeCertificateHandlerOnDispose); + } + } + + /// + /// Rest POST. + /// + /// Finalized Endpoint Query with parameters. + /// Optional header information for the request. + /// The raw data to post. + /// Optional time in seconds before request expires. + /// Optional bool. If its true, response data will be read from web request download handler. + /// Optional certificate handler for custom certificate verification + /// Optional bool. If true and is not null, will be disposed, when the underlying UnityWebRequest is disposed. + /// The response data. + public static async Task PostAsync( + string query, + byte[] bodyData, + Dictionary headers = null, + int timeout = -1, + bool readResponseData = false, + CertificateHandler certificateHandler = null, + bool disposeCertificateHandlerOnDispose = true, + CancellationToken cancellationToken = default(CancellationToken)) + { + using (var webRequest = UnityWebRequest.Post(query, "POST")) + { + cancellationToken.Register(() => + { + webRequest.Abort(); + }); + webRequest.uploadHandler = new UploadHandlerRaw(bodyData); + webRequest.downloadHandler = new DownloadHandlerBuffer(); + webRequest.SetRequestHeader("Content-Type", "application/octet-stream"); + return await ProcessRequestAsync(webRequest, timeout, headers, readResponseData, certificateHandler, disposeCertificateHandlerOnDispose); + } + } + + #endregion POST + + #region PUT + + /// + /// Rest PUT. + /// + /// Finalized Endpoint Query with parameters. + /// Data to be submitted. + /// Optional header information for the request. + /// Optional time in seconds before request expires. + /// Optional bool. If its true, response data will be read from web request download handler. + /// Optional certificate handler for custom certificate verification + /// Optional bool. If true and is not null, will be disposed, when the underlying UnityWebRequest is disposed. + /// The response data. + public static async Task PutAsync( + string query, + string jsonData, + Dictionary headers = null, + int timeout = -1, + bool readResponseData = false, + CertificateHandler certificateHandler = null, + bool disposeCertificateHandlerOnDispose = true, + CancellationToken cancellationToken = default(CancellationToken)) + { + using (var webRequest = UnityWebRequest.Put(query, jsonData)) + { + cancellationToken.Register(() => + { + webRequest.Abort(); + }); + webRequest.SetRequestHeader("Content-Type", "application/json"); + return await ProcessRequestAsync(webRequest, timeout, headers, readResponseData, certificateHandler, disposeCertificateHandlerOnDispose); + } + } + + /// + /// Rest PUT. + /// + /// Finalized Endpoint Query with parameters. + /// Data to be submitted. + /// Optional header information for the request. + /// Optional time in seconds before request expires. + /// Optional bool. If its true, response data will be read from web request download handler. + /// Optional certificate handler for custom certificate verification + /// Optional bool. If true and is not null, will be disposed, when the underlying UnityWebRequest is disposed. + /// The response data. + public static async Task PutAsync( + string query, + byte[] bodyData, + Dictionary headers = null, + int timeout = -1, + bool readResponseData = false, + CertificateHandler certificateHandler = null, + bool disposeCertificateHandlerOnDispose = true, + CancellationToken cancellationToken = default(CancellationToken)) + { + using (var webRequest = UnityWebRequest.Put(query, bodyData)) + { + cancellationToken.Register(() => + { + webRequest.Abort(); + }); + webRequest.SetRequestHeader("Content-Type", "application/octet-stream"); + return await ProcessRequestAsync(webRequest, timeout, headers, readResponseData, certificateHandler, disposeCertificateHandlerOnDispose); + } + } + + #endregion PUT + + #region DELETE + + /// + /// Rest DELETE. + /// + /// Finalized Endpoint Query with parameters. + /// Optional header information for the request. + /// Optional time in seconds before request expires. + /// Optional bool. If its true, response data will be read from web request download handler. + /// Optional certificate handler for custom certificate verification + /// Optional bool. If true and is not null, will be disposed, when the underlying UnityWebRequest is disposed. + /// The response data. + public static async Task DeleteAsync( + string query, + Dictionary headers = null, + int timeout = -1, + bool readResponseData = false, + CertificateHandler certificateHandler = null, + bool disposeCertificateHandlerOnDispose = true, + CancellationToken cancellationToken = default(CancellationToken)) + { + using (var webRequest = UnityWebRequest.Delete(query)) + { + cancellationToken.Register(() => + { + webRequest.Abort(); + }); + return await ProcessRequestAsync(webRequest, timeout, headers, readResponseData, certificateHandler, disposeCertificateHandlerOnDispose); + } + } + + #endregion DELETE + + private static async Task ProcessRequestAsync(UnityWebRequest webRequest, int timeout, Dictionary headers = null, bool readResponseData = false, CertificateHandler certificateHandler = null, bool disposeCertificateHandlerOnDispose = true) + { + if (timeout > 0) + { + webRequest.timeout = timeout; + } + + if (headers != null) + { + foreach (var header in headers) + { + webRequest.SetRequestHeader(header.Key, header.Value); + } + } + + // HACK: Workaround for extra quotes around boundary. + if (webRequest.method == UnityWebRequest.kHttpVerbPOST || + webRequest.method == UnityWebRequest.kHttpVerbPUT) + { + string contentType = webRequest.GetRequestHeader("Content-Type"); + if (contentType != null) + { + contentType = contentType.Replace("\"", ""); + webRequest.SetRequestHeader("Content-Type", contentType); + } + } + + webRequest.certificateHandler = certificateHandler; + webRequest.disposeCertificateHandlerOnDispose = disposeCertificateHandlerOnDispose; + await webRequest.SendWebRequest(); + + long responseCode = webRequest.responseCode; + Func downloadHandlerDataAction = () => webRequest.downloadHandler?.data; + Func downloadHandlerTextAction = () => webRequest.downloadHandler?.text; + +#if UNITY_2020_1_OR_NEWER + if (webRequest.result == UnityWebRequest.Result.ConnectionError || webRequest.result == UnityWebRequest.Result.ProtocolError) +#else + if (webRequest.isNetworkError || webRequest.isHttpError) +#endif // UNITY_2020_1_OR_NEWER + { + if (responseCode == 401) { return new Response(false, "Invalid Credentials", null, responseCode); } + + if (webRequest.GetResponseHeaders() == null) + { + return new Response(false, "Device Unavailable", null, responseCode); + } + + string responseHeaders = webRequest.GetResponseHeaders().Aggregate(string.Empty, (current, header) => $"\n{header.Key}: {header.Value}"); + string downloadHandlerText = downloadHandlerTextAction.Invoke(); + Debug.LogError($"REST Error: {responseCode}\n{downloadHandlerText}{responseHeaders}"); + return new Response(false, $"{responseHeaders}\n{downloadHandlerText}", downloadHandlerDataAction.Invoke(), responseCode); + } + + if (readResponseData) + { + return new Response(true, downloadHandlerTextAction.Invoke(), downloadHandlerDataAction.Invoke(), responseCode); + } + else // This option can be used only if action will be triggered in the same scope as the webrequest + { + return new Response(true, downloadHandlerTextAction, downloadHandlerDataAction, responseCode); + } + } + } +} diff --git a/com.microsoft.mrtk.buildwindow/WebRequestRest/Rest.cs.meta b/com.microsoft.mrtk.buildwindow/WebRequestRest/Rest.cs.meta new file mode 100644 index 00000000000..5f5e338a2e0 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WebRequestRest/Rest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fba7dca7fc2e4858820a4415bd4578b1 +timeCreated: 1663170292 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal.meta new file mode 100644 index 00000000000..c04c57896bb --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c5d3084336914139b002cc6e2df9a5a3 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures.meta new file mode 100644 index 00000000000..46f4199878c --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4ca7cc9afc9d4a4ea29fb14f26f89729 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ActivePowerSchemeInfo.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ActivePowerSchemeInfo.cs new file mode 100644 index 00000000000..cf7d1a15341 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ActivePowerSchemeInfo.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class ActivePowerSchemeInfo + { + public string ActivePowerScheme; + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ActivePowerSchemeInfo.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ActivePowerSchemeInfo.cs.meta new file mode 100644 index 00000000000..62aab7e1d2b --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ActivePowerSchemeInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d362de56ba6741ea99d65c404a2170a5 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/AdapterInfo.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/AdapterInfo.cs new file mode 100644 index 00000000000..6d8082fda44 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/AdapterInfo.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class AdapterInfo + { + public string Description; + public string HardwareAddress; + public int Index; + public string Name; + public string Type; + public DHCPInfo DHCP; + public IpAddressInfo[] Gateways; + public IpAddressInfo[] IpAddresses; + } +} diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/AdapterInfo.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/AdapterInfo.cs.meta new file mode 100644 index 00000000000..b5ef393ec58 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/AdapterInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 914c7d824d72497e8a19a5ef40ebe464 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ApplicationInfo.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ApplicationInfo.cs new file mode 100644 index 00000000000..0fcfab1da4a --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ApplicationInfo.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class ApplicationInfo + { + public string Name; + public string PackageFamilyName; + public string PackageFullName; + public int PackageOrigin; + public string PackageRelativeId; + public string Publisher; + } +} diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ApplicationInfo.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ApplicationInfo.cs.meta new file mode 100644 index 00000000000..49765e27199 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ApplicationInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 01999bf58f9740b6b700687464f849e0 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/AvailableWiFiNetworks.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/AvailableWiFiNetworks.cs new file mode 100644 index 00000000000..58b950d43e2 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/AvailableWiFiNetworks.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class AvailableWiFiNetworks + { + public WirelessNetworkInfo[] AvailableNetworks; + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/AvailableWiFiNetworks.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/AvailableWiFiNetworks.cs.meta new file mode 100644 index 00000000000..6faa39c527e --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/AvailableWiFiNetworks.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 10bed781f2644d508817650e1aeba971 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/BatteryInfo.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/BatteryInfo.cs new file mode 100644 index 00000000000..31a1d81f345 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/BatteryInfo.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class BatteryInfo + { + /// + /// (0 | 1) + /// + public int AcOnline; + /// + /// (0 | 1) + /// + public int BatteryPresent; + /// + /// (0 | 1) + /// + public int Charging; + public int DefaultAlert1; + public int DefaultAlert2; + public int EstimatedTime; + public int MaximumCapacity; + public int RemainingCapacity; + + public bool IsCharging => AcOnline != 0; + + [NonSerialized] + private float percentRemaining = 0f; + public float PercentRemaining + { + get + { + if (percentRemaining > 0f) + { + return percentRemaining; + } + + return percentRemaining = RemainingCapacity / (float)MaximumCapacity; + } + } + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/BatteryInfo.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/BatteryInfo.cs.meta new file mode 100644 index 00000000000..c5b74a74ecc --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/BatteryInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2753995a8a064f16a8c2177ffea5595b +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DHCPInfo.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DHCPInfo.cs new file mode 100644 index 00000000000..4858d4b7c5d --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DHCPInfo.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class DHCPInfo + { + public int LeaseExpires; + public int LeaseObtained; + public IpAddressInfo Address; + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DHCPInfo.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DHCPInfo.cs.meta new file mode 100644 index 00000000000..52eefdc24e4 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DHCPInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 74c8599f27f34d4ca6ff0bc3824e8665 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DeviceInfo.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DeviceInfo.cs new file mode 100644 index 00000000000..53108346699 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DeviceInfo.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class DeviceInfo + { + /// + /// Constant string for local machine target + /// + public const string LocalMachine = "Local Machine"; + + /// + /// Constant string for local machine IP Address + /// + public const string LocalIPAddress = "127.0.0.1"; + + // These fields are public to be serialized by the Unity Json Serializer Utility. + #region Json Serialized Fields + + /// + /// The IP Address of the device. + /// + public string IP; + + /// + /// The user name of the device. + /// + public string User; + + /// + /// The password for the device. + /// + public string Password; + + /// + /// The machine name of the device. + /// + public string MachineName; + + #endregion Json Serialized Fields + + // These fields are public but NonSerialized because we don't want them serialized by the + // Json Utility, but we also don't want their values overwritten when deserialization happens. + #region Json Overwritten Fields + + /// + /// The current CSRF Token for the device. + /// + [NonSerialized] + public string CsrfToken; + + private Dictionary authorization; + + #endregion Json Overwritten Fields + + // Properties are not serialized by the Unity JSON serializer, but become null whenever deserialized. + #region Properties + + /// + /// The current authorization for the device. + /// + public Dictionary Authorization => authorization ?? (authorization = new Dictionary { { "Authorization", Microsoft.MixedReality.Toolkit.Utilities.Rest.GetBasicAuthentication(User, Password) } }); + + /// + /// The last known battery state of the device. + /// + public BatteryInfo BatteryInfo { get; set; } + + /// + /// The last known power state of the device. + /// + public PowerStateInfo PowerState { get; set; } + + #endregion Properties + + /// + /// Constructor. + /// + public DeviceInfo(string ip, string user, string password, string machineName = "") + { + IP = ip; + User = user; + Password = password; + MachineName = machineName; + CsrfToken = string.Empty; + } + + public override string ToString() + { + return IP + (string.IsNullOrEmpty(MachineName) ? string.Empty : $" [{MachineName}]"); + } + } +} diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DeviceInfo.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DeviceInfo.cs.meta new file mode 100644 index 00000000000..582fe7da63a --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DeviceInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 23fd78cc18f64cc49f925146f2fc903d +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DeviceOsInfo.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DeviceOsInfo.cs new file mode 100644 index 00000000000..0f5230a0dc6 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DeviceOsInfo.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class DeviceOsInfo + { + public string ComputerName; + public string OsEdition; + public int OsEditionId; + public string OsVersion; + public string Platform; + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DeviceOsInfo.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DeviceOsInfo.cs.meta new file mode 100644 index 00000000000..9f6a18281ac --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DeviceOsInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9baf98dde2d641d28427e3b9a4e7172f +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DevicePortalConnections.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DevicePortalConnections.cs new file mode 100644 index 00000000000..c2695157fc7 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DevicePortalConnections.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + /// + /// Utility class to store a list of device connection info and track current one in use or selected + /// + [Serializable] + public class DevicePortalConnections + { + /// + /// List of device endpoints being tracked including ip address, authorization info, etc. + /// + public List Connections = new List(0); + + /// + /// Current or last targeted connection index in connection list + /// + public int CurrentConnectionIndex = 0; + + /// + /// Empty constructor + /// + public DevicePortalConnections() { } + + /// + /// Initialize + /// + public DevicePortalConnections(DeviceInfo deviceInfo) + { + Connections.Add(deviceInfo); + } + } +} diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DevicePortalConnections.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DevicePortalConnections.cs.meta new file mode 100644 index 00000000000..5bf7ef5a539 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/DevicePortalConnections.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 581fa728ee5145bf93eab2e5c363eb35 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/FileInfo.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/FileInfo.cs new file mode 100644 index 00000000000..4d72f32643a --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/FileInfo.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public struct FileInfo + { + /// + /// Folder under the requested known folder. + /// + public string CurrentDir; + public int DateCreated; + /// + /// In bytes. + /// + public int FileSize; + public string Id; + public string Name; + /// + /// Present if this item is a folder, this is the name of the folder. + /// + public string SubPath; + /// + /// Folder==16 + /// File==32 + /// + public int Type; + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/FileInfo.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/FileInfo.cs.meta new file mode 100644 index 00000000000..89520851c23 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/FileInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 050277c609e8457d8792b51de3e6f321 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/FileList.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/FileList.cs new file mode 100644 index 00000000000..8906d410ace --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/FileList.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class FileList + { + public FileInfo[] Items; + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/FileList.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/FileList.cs.meta new file mode 100644 index 00000000000..33ccb82911e --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/FileList.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bb28fe552fe34701b76c966a9a1604fc +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InstallStatus.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InstallStatus.cs new file mode 100644 index 00000000000..29c2bc7939f --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InstallStatus.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class InstallStatus + { + public int Code; + public string CodeText; + public string Reason; + public bool Success; + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InstallStatus.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InstallStatus.cs.meta new file mode 100644 index 00000000000..8b7e5eba1cd --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InstallStatus.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d8aeb56fd27148e2bdde9e2476071595 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InstalledApps.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InstalledApps.cs new file mode 100644 index 00000000000..bd6c2866e44 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InstalledApps.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class InstalledApps + { + public ApplicationInfo[] InstalledPackages; + } +} diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InstalledApps.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InstalledApps.cs.meta new file mode 100644 index 00000000000..7ea407da12c --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InstalledApps.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: dc78811ad0874ad68c88c2f2041a9520 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InterfaceInfo.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InterfaceInfo.cs new file mode 100644 index 00000000000..ac731312ef0 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InterfaceInfo.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class InterfaceInfo + { + public string Description; + public string GUID; + public int Index; + public NetworkProfileInfo[] ProfilesList; + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InterfaceInfo.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InterfaceInfo.cs.meta new file mode 100644 index 00000000000..e70fbed7dac --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/InterfaceInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 40500a5d265549c5ad325c12cfaefbd8 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/IpAddressInfo.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/IpAddressInfo.cs new file mode 100644 index 00000000000..cb889cef4fc --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/IpAddressInfo.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class IpAddressInfo + { + public string IpAddress; + public string Mask; + } +} diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/IpAddressInfo.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/IpAddressInfo.cs.meta new file mode 100644 index 00000000000..c1802b27431 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/IpAddressInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 007347a1ee544d3aad44b9f3255c4e96 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/IpConfigInfo.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/IpConfigInfo.cs new file mode 100644 index 00000000000..a319f79e1e1 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/IpConfigInfo.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class IpConfigInfo + { + public AdapterInfo[] Adapters; + } +} diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/IpConfigInfo.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/IpConfigInfo.cs.meta new file mode 100644 index 00000000000..137da31af51 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/IpConfigInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e6dd2737ca7f4aee8cb30cce6822ad5d +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/MachineName.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/MachineName.cs new file mode 100644 index 00000000000..426a36ec55e --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/MachineName.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class MachineName + { + public string ComputerName; + } +} diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/MachineName.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/MachineName.cs.meta new file mode 100644 index 00000000000..933608633e1 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/MachineName.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c05c11b304324027aa9ae48d6a760090 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/NetworkInterfaces.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/NetworkInterfaces.cs new file mode 100644 index 00000000000..6e5541cff0d --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/NetworkInterfaces.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class NetworkInterfaces + { + public InterfaceInfo[] Interfaces; + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/NetworkInterfaces.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/NetworkInterfaces.cs.meta new file mode 100644 index 00000000000..fa7bb01d161 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/NetworkInterfaces.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2a68c060f2214050a6bb0b4b1c975191 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/NetworkProfileInfo.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/NetworkProfileInfo.cs new file mode 100644 index 00000000000..adc32311125 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/NetworkProfileInfo.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class NetworkProfileInfo + { + public bool GroupPolicyProfile; + public string Name; + public bool PerUserProfile; + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/NetworkProfileInfo.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/NetworkProfileInfo.cs.meta new file mode 100644 index 00000000000..6b898328807 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/NetworkProfileInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 55c3f61dd86246b386ab9d2a92d1b578 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/PowerStateInfo.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/PowerStateInfo.cs new file mode 100644 index 00000000000..44857da2d60 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/PowerStateInfo.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class PowerStateInfo + { + public bool LowPowerState; + public bool LowPowerStateAvailable; + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/PowerStateInfo.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/PowerStateInfo.cs.meta new file mode 100644 index 00000000000..bd4c2058772 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/PowerStateInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ead7b6ed4f88433fa547bdc3c7b61c13 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ProcessInfo.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ProcessInfo.cs new file mode 100644 index 00000000000..418b07ceca5 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ProcessInfo.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class ProcessInfo + { + public float CPUUsage; + public string ImageName; + public float PageFileUsage; + public int PrivateWorkingSet; + public int ProcessId; + public int SessionId; + public string UserName; + public int VirtualSize; + public int WorkingSetSize; + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ProcessInfo.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ProcessInfo.cs.meta new file mode 100644 index 00000000000..49f64819b29 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ProcessInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 204abd5f3d7348418b89677589b41742 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ProcessList.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ProcessList.cs new file mode 100644 index 00000000000..26025929ee6 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ProcessList.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class ProcessList + { + public ProcessInfo[] Processes; + } +} \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ProcessList.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ProcessList.cs.meta new file mode 100644 index 00000000000..53a057ea643 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/ProcessList.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ff2bb9ec4a7e487082f2debe17b33e6e +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/WirelessNetworkInfo.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/WirelessNetworkInfo.cs new file mode 100644 index 00000000000..8309788b43d --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/WirelessNetworkInfo.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + [Serializable] + public class WirelessNetworkInfo + { + public bool AlreadyConnected; + public string AuthenticationAlgorithm; + public int Channel; + public string CipherAlgorithm; + /// + /// (0 | 1) + /// + public int Connectable; + public string InfrastructureType; + public bool ProfileAvailable; + public string ProfileName; + public string SSID; + /// + /// (0 | 1) + /// + public int SecurityEnabled; + public int SignalQuality; + public int[] BSSID; + public string[] PhysicalTypes; + } +} diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/WirelessNetworkInfo.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/WirelessNetworkInfo.cs.meta new file mode 100644 index 00000000000..24cc282b96d --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DataStructures/WirelessNetworkInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3f1ef00a581a4514b5b6ba29d8271292 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DevicePortal.cs b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DevicePortal.cs new file mode 100644 index 00000000000..8b30720d475 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DevicePortal.cs @@ -0,0 +1,909 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.MixedReality.Toolkit.Utilities; +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.Networking; +using Debug = UnityEngine.Debug; +using IOFileInfo = System.IO.FileInfo; +// This using exists to work around naming conflicts described in: +// https://github.com/microsoft/MixedRealityToolkit-Unity/issues/8104 +// If the internal rest class gets renamed this workaround can be removed. +using RestHelpers = Microsoft.MixedReality.Toolkit.Utilities; + +namespace Microsoft.MixedReality.Toolkit.WindowsDevicePortal +{ + /// + /// Function used to communicate with Windows 10 devices through the device portal REST APIs. + /// + public static class DevicePortal + { + /// + /// Use SSL Connections when making rest calls. + /// + public static bool UseSSL { get; set; } = true; + + /// + /// Use SSL Certificate Verification when making SSL rest calls. + /// + public static bool VerifySSLCertificates { get; set; } = true; + + private enum AppInstallStatus + { + Invalid, + Installing, + InstallSuccess, + InstallFail + } + + /// + /// Custom certificate handler for device portal request. + /// The device portal on HoloLens uses a self-signed certificate, therefore SSL Unity WebRequest will fail. + /// As a fix we simply accept all certificates, including self-signed, without further checking, + /// as they do not chain up to any Microsoft Root Certificate. + /// + private class BlankCertificateHandler : CertificateHandler + { + protected override bool ValidateCertificate(byte[] certificateData) + { + // Accept all the certificates! + return true; + } + } + + private static readonly BlankCertificateHandler BlankCertificateHandlerInstance = new BlankCertificateHandler(); + + private static CertificateHandler DevicePortalCertificateHandler + { + get { return !VerifySSLCertificates ? BlankCertificateHandlerInstance : null; } + } + + // Device Portal API Resources + // https://docs.microsoft.com/windows/uwp/debug-test-perf/device-portal-api-hololens#holographic-os + // https://docs.microsoft.com/windows/uwp/debug-test-perf/device-portal-api-core + private const string GetDeviceOsInfoQuery = @"{0}/api/os/info"; + private const string GetMachineNameQuery = @"{0}/api/os/machinename"; + private const string GetBatteryQuery = @"{0}/api/power/battery"; + private const string GetPowerStateQuery = @"{0}/api/power/state"; + private const string RestartDeviceQuery = @"{0}/api/control/restart"; + private const string ShutdownDeviceQuery = @"{0}/api/control/shutdown"; + private const string ProcessQuery = @"{0}/api/resourcemanager/processes"; + private const string AppQuery = @"{0}/api/taskmanager/app"; + private const string PackagesQuery = @"{0}/api/appx/packagemanager/packages"; + private const string InstallQuery = @"{0}/api/app/packagemanager/package"; + private const string InstallStatusQuery = @"{0}/api/app/packagemanager/state"; + private const string FileQuery = @"{0}/api/filesystem/apps/file?knownfolderid=LocalAppData&filename=UnityPlayer.log&packagefullname={1}&path=%5C%5CTempState"; + private const string IpConfigQuery = @"{0}/api/networking/ipconfig"; + private const string WiFiNetworkQuery = @"{0}/api/wifi/network{1}"; + private const string WiFiInterfacesQuery = @"{0}/api/wifi/interfaces"; + +#if !UNITY_WSA || UNITY_EDITOR + /// + /// Opens the Device Portal for the target device. + /// + public static void OpenWebPortal(DeviceInfo targetDevice) + { + System.Diagnostics.Process.Start(FinalizeUrl(targetDevice.IP)); + } +#endif + + /// + /// Gets the of the target device. + /// + /// + public static async Task GetDeviceOsInfoAsync(DeviceInfo targetDevice) + { + var isAuth = await EnsureAuthenticationAsync(targetDevice); + if (!isAuth) { return null; } + + string query = string.Format(GetDeviceOsInfoQuery, FinalizeUrl(targetDevice.IP)); + var response = await RestHelpers.Rest.GetAsync(query, targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (!response.Successful) + { + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + return await GetDeviceOsInfoAsync(targetDevice); + } + + Debug.LogError(response.ResponseBody); + return null; + } + + return JsonUtility.FromJson(response.ResponseBody); + } + + /// + /// Gets the of the target device. + /// + /// + public static async Task GetMachineNameAsync(DeviceInfo targetDevice) + { + var isAuth = await EnsureAuthenticationAsync(targetDevice); + if (!isAuth) { return null; } + + string query = string.Format(GetMachineNameQuery, FinalizeUrl(targetDevice.IP)); + var response = await RestHelpers.Rest.GetAsync(query, targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (!response.Successful) + { + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + return await GetMachineNameAsync(targetDevice); + } + + Debug.LogError(response.ResponseBody); + return null; + } + + return JsonUtility.FromJson(response.ResponseBody); + } + + /// + /// Gets the of the target device. + /// + /// + public static async Task GetBatteryStateAsync(DeviceInfo targetDevice) + { + var isAuth = await EnsureAuthenticationAsync(targetDevice); + if (!isAuth) { return null; } + + string query = string.Format(GetBatteryQuery, FinalizeUrl(targetDevice.IP)); + var response = await RestHelpers.Rest.GetAsync(query, targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (!response.Successful) + { + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + return await GetBatteryStateAsync(targetDevice); + } + + Debug.LogError(response.ResponseBody); + return null; + } + + return JsonUtility.FromJson(response.ResponseBody); + } + + /// + /// Gets the of the target device. + /// + /// + public static async Task GetPowerStateAsync(DeviceInfo targetDevice) + { + var isAuth = await EnsureAuthenticationAsync(targetDevice); + if (!isAuth) { return null; } + + string query = string.Format(GetPowerStateQuery, FinalizeUrl(targetDevice.IP)); + var response = await RestHelpers.Rest.GetAsync(query, targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (!response.Successful) + { + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + await GetPowerStateAsync(targetDevice); + } + return null; + } + + return JsonUtility.FromJson(response.ResponseBody); + } + + /// + /// Restart the target device. + /// + /// True, if the device has successfully restarted. + public static async Task RestartAsync(DeviceInfo targetDevice) + { + var isAuth = await EnsureAuthenticationAsync(targetDevice); + if (!isAuth) { return false; } + + var response = await RestHelpers.Rest.PostAsync(string.Format(RestartDeviceQuery, FinalizeUrl(targetDevice.IP)), targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (response.Successful) + { + bool hasRestarted = false; + string query = string.Format(GetPowerStateQuery, FinalizeUrl(targetDevice.IP)); + + while (!hasRestarted) + { + response = await RestHelpers.Rest.GetAsync(query, targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (!response.Successful) + { + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + continue; + } + + Debug.LogError(response.ResponseBody); + return false; + } + + hasRestarted = response.Successful; + } + + return true; + } + + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + await RestartAsync(targetDevice); + } + + Debug.LogError(response.ResponseBody); + return false; + } + + /// + /// Shuts down the target device. + /// + /// True, if the device is shutting down. + public static async Task ShutdownAsync(DeviceInfo targetDevice) + { + var isAuth = await EnsureAuthenticationAsync(targetDevice); + if (!isAuth) { return false; } + + var response = await RestHelpers.Rest.PostAsync(string.Format(ShutdownDeviceQuery, FinalizeUrl(targetDevice.IP)), targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (!response.Successful) + { + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + return await ShutdownAsync(targetDevice); + } + + Debug.LogError(response.ResponseBody); + return false; + } + + return true; + } + + /// + /// Determines if the target application is currently running on the target device. + /// + /// True, if application is currently installed on device. + public static async Task IsAppInstalledAsync(string packageName, DeviceInfo targetDevice) + { + Debug.Assert(!string.IsNullOrEmpty(packageName)); + return await GetApplicationInfoAsync(packageName, targetDevice) != null; + } + + /// + /// Determines if the target application is running on the target device. + /// + /// Optional cached . + /// True, if the application is running. + public static async Task IsAppRunningAsync(string packageName, DeviceInfo targetDevice, ApplicationInfo appInfo = null) + { + Debug.Assert(!string.IsNullOrEmpty(packageName)); + + if (appInfo == null) + { + appInfo = await GetApplicationInfoAsync(packageName, targetDevice); + } + + if (appInfo == null) + { + Debug.LogError($"{packageName} not installed."); + return false; + } + + var response = await RestHelpers.Rest.GetAsync(string.Format(ProcessQuery, FinalizeUrl(targetDevice.IP)), targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (response.Successful) + { + var processList = JsonUtility.FromJson(response.ResponseBody); + for (int i = 0; i < processList.Processes.Length; ++i) + { + if (processList.Processes[i].ImageName.Contains(appInfo.Name)) + { + return true; + } + } + + return false; + } + + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + return await IsAppRunningAsync(packageName, targetDevice, appInfo); + } + + Debug.LogError($"{response.ResponseBody}"); + return false; + } + + /// + /// Gets the of the target application on the target device. + /// + /// Returns the of the target application from the target device. + private static async Task GetApplicationInfoAsync(string packageName, DeviceInfo targetDevice) + { + Debug.Assert(!string.IsNullOrEmpty(packageName)); + var appList = await GetAllInstalledAppsAsync(targetDevice); + + for (int i = 0; i < appList?.InstalledPackages.Length; ++i) + { + if (appList.InstalledPackages[i].PackageFullName.Equals(packageName, StringComparison.OrdinalIgnoreCase)) + { + return appList.InstalledPackages[i]; + } + + if (appList.InstalledPackages[i].PackageFamilyName.Equals(packageName, StringComparison.OrdinalIgnoreCase)) + { + return appList.InstalledPackages[i]; + } + } + + return null; + } + + public static async Task GetAllInstalledAppsAsync(DeviceInfo targetDevice) + { + var isAuth = await EnsureAuthenticationAsync(targetDevice); + if (!isAuth) { return null; } + + var response = await RestHelpers.Rest.GetAsync(string.Format(PackagesQuery, FinalizeUrl(targetDevice.IP)), targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (!response.Successful) + { + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + return await GetAllInstalledAppsAsync(targetDevice); + } + + Debug.LogError(response.ResponseBody); + return null; + } + + return JsonUtility.FromJson(response.ResponseBody); + } + + /// + /// Installs the target application on the target device. + /// + /// Should the thread wait until installation is complete? + /// True, if Installation was a success. + public static async Task InstallAppAsync(string appFullPath, DeviceInfo targetDevice, bool waitForDone = true) + { + Debug.Assert(!string.IsNullOrEmpty(appFullPath)); + var isAuth = await EnsureAuthenticationAsync(targetDevice); + if (!isAuth) + { + return false; + } + + Debug.Log($"Starting app install on {targetDevice.ToString()}..."); + + // Calculate the cert and dependency paths + string fileName = Path.GetFileName(appFullPath); + string certFullPath = Path.ChangeExtension(appFullPath, ".cer"); + string certName = Path.GetFileName(certFullPath); + + string arch = "ARM"; + if (appFullPath.Contains("x86")) + { + arch = "x86"; + } + else if (appFullPath.Contains("ARM64")) + { + arch = "ARM64"; + } + + string depPath = $@"{Path.GetDirectoryName(appFullPath)}\Dependencies\{arch}\"; + + var form = new WWWForm(); + + try + { + // APPX file + Debug.Assert(appFullPath != null); + using (var stream = new FileStream(appFullPath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + using (var reader = new BinaryReader(stream)) + { + form.AddBinaryData(fileName, reader.ReadBytes((int)reader.BaseStream.Length), fileName); + } + } + + // CERT file + Debug.Assert(certFullPath != null); + using (var stream = new FileStream(certFullPath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + using (var reader = new BinaryReader(stream)) + { + form.AddBinaryData(certName, reader.ReadBytes((int)reader.BaseStream.Length), certName); + } + } + + // Dependencies + IOFileInfo[] depFiles = new DirectoryInfo(depPath).GetFiles(); + foreach (IOFileInfo dep in depFiles) + { + using (var stream = new FileStream(dep.FullName, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + using (var reader = new BinaryReader(stream)) + { + string depFilename = Path.GetFileName(dep.FullName); + form.AddBinaryData(depFilename, reader.ReadBytes((int)reader.BaseStream.Length), depFilename); + } + } + } + } + catch (Exception e) + { + Debug.LogException(e); + return false; + } + + // Query + string query = $"{string.Format(InstallQuery, FinalizeUrl(targetDevice.IP))}?package={UnityWebRequest.EscapeURL(fileName)}"; + + var response = await RestHelpers.Rest.PostAsync(query, form, targetDevice.Authorization, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (!response.Successful) + { + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + return await InstallAppAsync(appFullPath, targetDevice, waitForDone); + } + + Debug.LogError($"Failed to install {fileName} on {targetDevice.ToString()}."); + return false; + } + + var status = AppInstallStatus.Installing; + + // Wait for done (if requested) + while (waitForDone && status == AppInstallStatus.Installing) + { + status = await GetInstallStatusAsync(targetDevice); + + switch (status) + { + case AppInstallStatus.InstallSuccess: + Debug.Log($"Successfully installed {fileName} on {targetDevice.ToString()}."); + return true; + case AppInstallStatus.InstallFail: + Debug.LogError($"Failed to install {fileName} on {targetDevice.ToString()}."); + return false; + } + } + + return true; + } + + private static async Task GetInstallStatusAsync(DeviceInfo targetDevice) + { + var response = await RestHelpers.Rest.GetAsync(string.Format(InstallStatusQuery, FinalizeUrl(targetDevice.IP)), targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (response.Successful) + { + var status = JsonUtility.FromJson(response.ResponseBody); + + if (status == null) + { + return AppInstallStatus.Installing; + } + + if (status.Success) + { + return AppInstallStatus.InstallSuccess; + } + + Debug.LogError($"{status.Reason}\n{status.CodeText}"); + } + else + { + return AppInstallStatus.Installing; + } + + return AppInstallStatus.InstallFail; + } + + /// + /// Uninstalls the target application on the target device + /// + /// Optional cached . + /// True, if uninstall was a success. + public static async Task UninstallAppAsync(string packageName, DeviceInfo targetDevice, ApplicationInfo appInfo = null) + { + Debug.Assert(!string.IsNullOrEmpty(packageName)); + + if (appInfo == null) + { + appInfo = await GetApplicationInfoAsync(packageName, targetDevice); + } + + if (appInfo == null) + { + Debug.LogWarning($"Application '{packageName}' not found"); + return false; + } + + Debug.Log($"Attempting to uninstall {packageName} on {targetDevice.ToString()}..."); + + string query = $"{string.Format(InstallQuery, FinalizeUrl(targetDevice.IP))}?package={UnityWebRequest.EscapeURL(appInfo.PackageFullName)}"; + var response = await RestHelpers.Rest.DeleteAsync(query, targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (response.Successful) + { + Debug.Log($"Successfully uninstalled {packageName} on {targetDevice.ToString()}."); + } + else + if (!response.Successful) + { + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + return await UninstallAppAsync(packageName, targetDevice); + } + + Debug.LogError($"Failed to uninstall {packageName} on {targetDevice.ToString()}"); + Debug.LogError(response.ResponseBody); + return false; + } + + return true; + } + + /// + /// Launches the target application on the target device. + /// + /// Optional cached . + /// True, if application was successfully launched and is currently running on the target device. + public static async Task LaunchAppAsync(string packageName, DeviceInfo targetDevice, ApplicationInfo appInfo = null) + { + Debug.Assert(!string.IsNullOrEmpty(packageName)); + + if (appInfo == null) + { + appInfo = await GetApplicationInfoAsync(packageName, targetDevice); + } + + if (appInfo == null) + { + Debug.LogWarning($"Application '{packageName}' not found"); + return false; + } + + string query = $"{string.Format(AppQuery, FinalizeUrl(targetDevice.IP))}?appid={UnityWebRequest.EscapeURL(appInfo.PackageRelativeId.EncodeTo64())}&package={UnityWebRequest.EscapeURL(appInfo.PackageFullName)}"; + var response = await RestHelpers.Rest.PostAsync(query, targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (!response.Successful) + { + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + return await LaunchAppAsync(packageName, targetDevice); + } + + Debug.LogError($"{response.ResponseCode}|{response.ResponseBody}"); + return false; + } + + while (!await IsAppRunningAsync(packageName, targetDevice, appInfo)) + { + await new WaitForSeconds(1f); + } + + return true; + } + + /// + /// Stops the target application on the target device. + /// + /// Optional cached . + /// true, if application was successfully stopped. + public static async Task StopAppAsync(string packageName, DeviceInfo targetDevice, ApplicationInfo appInfo = null) + { + Debug.Assert(!string.IsNullOrEmpty(packageName)); + + if (appInfo == null) + { + appInfo = await GetApplicationInfoAsync(packageName, targetDevice); + } + + if (appInfo == null) + { + Debug.LogWarning($"Application '{packageName}' not found"); + return false; + } + + string query = $"{string.Format(AppQuery, FinalizeUrl(targetDevice.IP))}?package={UnityWebRequest.EscapeURL(appInfo.PackageFullName.EncodeTo64())}"; + Response response = await RestHelpers.Rest.DeleteAsync(query, targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (!response.Successful) + { + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + return await StopAppAsync(packageName, targetDevice); + } + + Debug.LogError(response.ResponseBody); + return false; + } + + while (!await IsAppRunningAsync(packageName, targetDevice, appInfo)) + { + await new WaitForSeconds(1f); + } + + return true; + } + + /// + /// Downloads and launches the Log file for the target application on the target device. + /// + /// Optional cached . + /// The path of the downloaded log file. + public static async Task DownloadLogFileAsync(string packageName, DeviceInfo targetDevice, ApplicationInfo appInfo = null) + { + Debug.Assert(!string.IsNullOrEmpty(packageName)); + + if (appInfo == null) + { + appInfo = await GetApplicationInfoAsync(packageName, targetDevice); + } + + if (appInfo == null) + { + Debug.LogWarning($"Application '{packageName}' not found"); + return string.Empty; + } + + string logFile = $"{Application.temporaryCachePath}/{targetDevice.MachineName}_{DateTime.Now.Year}{DateTime.Now.Month}{DateTime.Now.Day}{DateTime.Now.Hour}{DateTime.Now.Minute}{DateTime.Now.Second}_player.txt"; + var response = await RestHelpers.Rest.GetAsync(string.Format(FileQuery, FinalizeUrl(targetDevice.IP), appInfo.PackageFullName), targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (!response.Successful) + { + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + return await DownloadLogFileAsync(packageName, targetDevice); + } + + Debug.LogError(response.ResponseBody); + return string.Empty; + } + + File.WriteAllText(logFile, response.ResponseBody); + return logFile; + + } + + /// + /// Gets the of the target device. + /// + /// + public static async Task GetIpConfigInfoAsync(DeviceInfo targetDevice) + { + var isAuth = await EnsureAuthenticationAsync(targetDevice); + if (!isAuth) { return null; } + + string query = string.Format(IpConfigQuery, FinalizeUrl(targetDevice.IP)); + var response = await RestHelpers.Rest.GetAsync(query, targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (!response.Successful) + { + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + return await GetIpConfigInfoAsync(targetDevice); + } + + Debug.LogError(response.ResponseBody); + return null; + } + + return JsonUtility.FromJson(response.ResponseBody); + } + + /// + /// Gets the of the target device. + /// + /// The GUID for the network interface to use to search for wireless networks, without brackets. + /// + public static async Task GetAvailableWiFiNetworksAsync(DeviceInfo targetDevice, InterfaceInfo interfaceInfo) + { + var isAuth = await EnsureAuthenticationAsync(targetDevice); + if (!isAuth) { return null; } + + string query = string.Format(WiFiNetworkQuery, FinalizeUrl(targetDevice.IP), $"s?interface={interfaceInfo.GUID}"); + var response = await RestHelpers.Rest.GetAsync(query, targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (!response.Successful) + { + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + return await GetAvailableWiFiNetworksAsync(targetDevice, interfaceInfo); + } + + Debug.LogError(response.ResponseBody); + return null; + } + + return JsonUtility.FromJson(response.ResponseBody); + } + + /// + /// Connects to the specified WiFi Network. + /// + /// The interface to use to connect. + /// The network to connect to. + /// Password for network access. + /// True, if connection successful. + public static async Task ConnectToWiFiNetworkAsync(DeviceInfo targetDevice, InterfaceInfo interfaceInfo, WirelessNetworkInfo wifiNetwork, string password) + { + var isAuth = await EnsureAuthenticationAsync(targetDevice); + if (!isAuth) { return new Response(false, "Unable to authenticate with device", null, 403); } + + string query = string.Format( + WiFiNetworkQuery, + FinalizeUrl(targetDevice.IP), + $"?interface={interfaceInfo.GUID}&ssid={wifiNetwork.SSID.EncodeTo64()}&op=connect&createprofile=yes&key={password}"); + return await RestHelpers.Rest.PostAsync(query, targetDevice.Authorization, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + } + + /// + /// Gets the of the target device. + /// + /// + public static async Task GetWiFiNetworkInterfacesAsync(DeviceInfo targetDevice) + { + var isAuth = await EnsureAuthenticationAsync(targetDevice); + if (!isAuth) { return null; } + + string query = string.Format(WiFiInterfacesQuery, FinalizeUrl(targetDevice.IP)); + var response = await RestHelpers.Rest.GetAsync(query, targetDevice.Authorization, readResponseData: true, certificateHandler: DevicePortalCertificateHandler, disposeCertificateHandlerOnDispose: false); + + if (!response.Successful) + { + if (response.ResponseCode == 403 && await RefreshCsrfTokenAsync(targetDevice)) + { + return await GetWiFiNetworkInterfacesAsync(targetDevice); + } + + Debug.LogError(response.ResponseBody); + return null; + } + + return JsonUtility.FromJson(response.ResponseBody); + } + + /// + /// This Utility method finalizes the URL and formats the HTTPS string if needed. + /// + /// Local Machine will be changed to 127.0.0.1:10080 for HoloLens connections. + /// The target URL i.e. 128.128.128.128 + /// The finalized URL with http/https prefix. + public static string FinalizeUrl(string targetUrl) + { + string ssl = DevicePortal.UseSSL ? "s" : string.Empty; + + if (targetUrl.Contains(DeviceInfo.LocalMachine) || targetUrl.Contains(DeviceInfo.LocalIPAddress)) + { + targetUrl = UseSSL ? $"{DeviceInfo.LocalIPAddress}:10443" : $"{DeviceInfo.LocalIPAddress}:10080"; + } + + return $@"http{ssl}://{targetUrl}"; + } + + /// + /// Refreshes the CSRF Token in case the device or its portal was restarted. + /// + /// True, if refresh was successful. + public static async Task RefreshCsrfTokenAsync(DeviceInfo targetDevice) + { + if (!targetDevice.Authorization.ContainsKey("cookie")) + { + Debug.LogError("Resetting Auth failed!"); + return false; + } + + targetDevice.Authorization.Remove("cookie"); + + return await EnsureAuthenticationAsync(targetDevice); + } + + /// + /// Makes sure the Authentication Headers and CSRF Tokens are set. + /// + /// True if Authentication is successful, otherwise false. + public static async Task EnsureAuthenticationAsync(DeviceInfo targetDevice) + { + targetDevice.Authorization["Authorization"] = RestHelpers.Rest.GetBasicAuthentication(targetDevice.User, targetDevice.Password); + + bool success; + + if (!targetDevice.Authorization.ContainsKey("cookie")) + { + var response = await DevicePortalAuthorizationAsync(targetDevice); + success = response.Successful; + + if (success) + { + // If null, authentication succeeded but we had no cookie token in the response. + // This usually means Unity has a cached token, so it can be ignored. + if (response.ResponseBody != null) + { + targetDevice.CsrfToken = response.ResponseBody; + + // Strip the beginning of the cookie header + targetDevice.CsrfToken = targetDevice.CsrfToken.Replace("CSRF-Token=", string.Empty); + } + } + else + { + Debug.LogError($"Authentication failed! {response.ResponseBody}"); + } + + if (!string.IsNullOrEmpty(targetDevice.CsrfToken)) + { + if (!targetDevice.Authorization.ContainsKey("cookie")) + { + targetDevice.Authorization.Add("cookie", targetDevice.CsrfToken); + } + else + { + targetDevice.Authorization["cookie"] = targetDevice.CsrfToken; + } + + if (targetDevice.Authorization.ContainsKey("x-csrf-token")) + { + targetDevice.Authorization["x-csrf-token"] = targetDevice.CsrfToken; + } + else + { + targetDevice.Authorization.Add("x-csrf-token", targetDevice.CsrfToken); + } + } + } + else + { + success = true; + } + + return success; + } + + private static async Task DevicePortalAuthorizationAsync(DeviceInfo targetDevice) + { + UnityWebRequest webRequest = UnityWebRequest.Get(FinalizeUrl(targetDevice.IP)); + + webRequest.timeout = 5; + webRequest.certificateHandler = DevicePortalCertificateHandler; + webRequest.disposeCertificateHandlerOnDispose = false; + webRequest.SetRequestHeader("Authorization", targetDevice.Authorization["Authorization"]); + + await webRequest.SendWebRequest(); + + long responseCode = webRequest.responseCode; +#if UNITY_2020_1_OR_NEWER + if (webRequest.result == UnityWebRequest.Result.ConnectionError || webRequest.result == UnityWebRequest.Result.ProtocolError) +#else + if (webRequest.isNetworkError || webRequest.isHttpError) +#endif // UNITY_2020_1_OR_NEWER + { + if (responseCode == 401) + { + return new Response(false, "Invalid Credentials", null, responseCode); + } + + if (webRequest.GetResponseHeaders() == null) + { + return new Response(false, "Device Not Found | No Response Headers", null, responseCode); + } + + string responseHeaders = webRequest.GetResponseHeaders().Aggregate(string.Empty, (current, header) => $"\n{header.Key}: {header.Value}"); + string downloadHandlerText = webRequest.downloadHandler?.text; + Debug.LogError($"REST Auth Error: {responseCode}\n{downloadHandlerText}{responseHeaders}"); + return new Response(false, $"{downloadHandlerText}", webRequest.downloadHandler?.data, responseCode); + } + + return new Response(true, () => webRequest.GetResponseHeader("Set-Cookie"), () => webRequest.downloadHandler?.data, responseCode); + } + } +} diff --git a/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DevicePortal.cs.meta b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DevicePortal.cs.meta new file mode 100644 index 00000000000..e9b84647f0e --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/WindowsDevicePortal/DevicePortal.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b0e494ca2bae4735b67a7011ef88fe81 +timeCreated: 1663169447 \ No newline at end of file diff --git a/com.microsoft.mrtk.buildwindow/package.json b/com.microsoft.mrtk.buildwindow/package.json new file mode 100644 index 00000000000..11a35c57694 --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/package.json @@ -0,0 +1,21 @@ +{ + "name": "com.microsoft.mrtk.buildwindow", + "version": "3.0.0-development", + "description": "Build window for generating and deploying appx from within Unity", + "displayName": "MRTK Build Window", + "msftFeatureCategory": "MRTK3", + "author": "Microsoft", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/MixedRealityToolkit-Unity.git" + }, + "bugs": { + "url": "https://github.com/microsoft/MixedRealityToolkit-Unity/issues" + }, + "unity": "2020.3", + "dependencies": { + }, + "msftOptionalPackages": { + } +} diff --git a/com.microsoft.mrtk.buildwindow/package.json.meta b/com.microsoft.mrtk.buildwindow/package.json.meta new file mode 100644 index 00000000000..059a1ebfc9b --- /dev/null +++ b/com.microsoft.mrtk.buildwindow/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ffb7c0cabb1cef942ae3cf1eab68f941 +PackageManifestImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: