diff --git a/App.config b/App.config new file mode 100644 index 0000000..3c88dd4 --- /dev/null +++ b/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/DevicePortalTool.csproj b/DevicePortalTool.csproj new file mode 100644 index 0000000..d83decc --- /dev/null +++ b/DevicePortalTool.csproj @@ -0,0 +1,117 @@ + + + + + Debug + AnyCPU + {A1A457E2-2559-4400-9C37-9ABC3D8D5D35} + Exe + DevicePortalTool + DevicePortalTool + v4.5.2 + 512 + true + true + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + DevicePortalTool.Program + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DevicePortalTool.sln b/DevicePortalTool.sln new file mode 100644 index 0000000..53e5de0 --- /dev/null +++ b/DevicePortalTool.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.168 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevicePortalTool", "DevicePortalTool.csproj", "{A1A457E2-2559-4400-9C37-9ABC3D8D5D35}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1A457E2-2559-4400-9C37-9ABC3D8D5D35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1A457E2-2559-4400-9C37-9ABC3D8D5D35}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1A457E2-2559-4400-9C37-9ABC3D8D5D35}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1A457E2-2559-4400-9C37-9ABC3D8D5D35}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5F79B122-3A33-472D-907F-74ABFAF87B99} + EndGlobalSection +EndGlobal diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..63caae8 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DevicePortalTool")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Unity Technologies")] +[assembly: AssemblyProduct("DevicePortalTool")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("a1a457e2-2559-4400-9c37-9abc3d8d5d35")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/README.md b/README.md new file mode 100644 index 0000000..057df14 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# DevicePortalTool +A command line program to execute Windows Device Portal REST APIs on a remote Windows 10 device, intended for use within an automated toolchain. +This program was developed using the [WindowsDevicePortalWrapper project](https://github.com/Microsoft/WindowsDevicePortalWrapper) provided by Microsoft. + diff --git a/Source/AppOperation.cs b/Source/AppOperation.cs new file mode 100644 index 0000000..b75f842 --- /dev/null +++ b/Source/AppOperation.cs @@ -0,0 +1,734 @@ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Tools.WindowsDevicePortal; +using static Microsoft.Tools.WindowsDevicePortal.DevicePortal; + +namespace DevicePortalTool +{ + class AppOperation + { + public enum Operation + { + None, + ListInstalledApps, + Install, + Run, + Uninstall, + } + + public static readonly string AvailableOperationsText = + "Supported App operations are the following:\n" + + " list\n" + + " install\n" + + " run\n" + + " uninstall\n" + ; + + public static readonly string AppOperationUsageText = + "Execute an Application operation on the remote device:\n" + + " /op: []\n" + + " Executes the app operation with operation-specific parameters\n" + + "\n" + + " /op: /?\n" + + " Shows usage for specified operation\n" + + "\n" + + AvailableOperationsText + ; + + public static readonly string ListOpUsageText = + "Lists the apps currently installed on the device\n" + ; + + public static readonly string InstallOpUsageText = + "Installs the specified app package to the device and optionally runs the specified app\n" + + " /appx: [/cert:] [/launch]\n" + + " Installs the given AppX package and optionally launches the app on the remote device\n" + ; + + public static readonly string RunOpUsageText = + "Runs the specified app on the device\n" + + " /package: /aumid:\n" + + " Launches the app installed specified by package and aumid on the remote device\n" + ; + + public static readonly string UninstallOpUsageText = + "Uninstall the specified app from the device\n" + + " /package:\n" + + " Uninstall the app matching the specified full package name from the device\n" + ; + + public static readonly string ListAppsOpUsageTExt = + "Outputs a list of installed app packages on the device to stdout\n" + ; + + public static readonly string ParameterAppx = "appx"; + public static readonly string ParameterCert = "cert"; + public static readonly string ParameterLaunch = "launch"; + public static readonly string parameterPackage = "package"; + public static readonly string ParameterAumid = "aumid"; + + public static Operation OperationStringToEnum(string operationName) + { + if (String.IsNullOrWhiteSpace(operationName)) + { + return Operation.None; + } + if (operationName.Equals("list", StringComparison.OrdinalIgnoreCase)) + { + return Operation.ListInstalledApps; + } + if (operationName.Equals("install", StringComparison.OrdinalIgnoreCase)) + { + return Operation.Install; + } + if (operationName.Equals("run", StringComparison.OrdinalIgnoreCase)) + { + return Operation.Run; + } + if (operationName.Equals("uninstall", StringComparison.OrdinalIgnoreCase)) + { + return Operation.Uninstall; + } + + return Operation.None; + } + + public static string OperationSpecificUsageText(Operation op) + { + switch (op) + { + case Operation.Install: + return InstallOpUsageText; + + case Operation.Run: + return RunOpUsageText; + + case Operation.Uninstall: + return UninstallOpUsageText; + + default: return String.Empty; + } + } + + public AppOperation(DevicePortal portal) + { + if (portal == null) + throw new System.ArgumentNullException("Must specify a valid DevicePortal object"); + + _portal = portal; + _runningOperation = new SemaphoreSlim(1, 1); + } + + public void ExecuteOperation(Operation op, ParameterHelper parameters) + { + ExecuteOperationInternal(op, parameters); + } + + public static bool TryExecuteApplicationOperation(DevicePortal portal, ParameterHelper parameters) + { + try + { + var appOperation = new AppOperation(portal); + appOperation.ExecuteOperation(AppOperation.OperationStringToEnum(parameters.GetParameterValue(ParameterHelper.Operation)), parameters); + } + catch (Exception) + { + return false; + } + return true; + } + + + // Class fields + private DevicePortal _portal; + private SemaphoreSlim _runningOperation; + + // During "install" operation contains the Identity data extracted from appx Manifest + private AppPackageIdentity _installedAppId; + + private bool _verbose; + private bool _helpFlag; + + private ApplicationInstallStatusEventArgs _lastInstallStatus; + + // Internal methods + private void ExecuteOperationInternal(Operation op, ParameterHelper parameters) + { + if (!_runningOperation.Wait(0)) + { + throw new SemaphoreFullException("Existing operation still running"); + } + + _verbose = parameters.HasFlag(ParameterHelper.VerboseFlag); + _helpFlag = parameters.HasFlag(ParameterHelper.HelpFlag); + + try + { + if (_helpFlag) + { + OutputOperationSpecificUsageText(op); + return; + } + + switch (op) + { + case Operation.ListInstalledApps: + ExecuteListInstalledAppsOperation(parameters); + break; + + case Operation.Install: + ExecuteInstallOperation(parameters); + + // Optionally run app on the device if "/launch" switch used + if (parameters.HasFlag(ParameterLaunch)) + { + ExecuteRunOperation(parameters); + } + break; + + case Operation.Run: + ExecuteRunOperation(parameters); + break; + + case Operation.Uninstall: + ExecuteUninstallyOperation(parameters); + break; + + default: + OutputDefaultUsageText(); + break; + } + } + catch (Exception ex) + { + var errorMessage = new StringBuilder(); + if (_verbose) + { + errorMessage.Append(ex.ToString()); + errorMessage.Append("\n\n"); + } + else + { + string message = ex.Message; + if (String.IsNullOrWhiteSpace(message)) + { + message = "No exception message"; + } + errorMessage.Append("App operation '" + op.ToString() + "' failed: " + message + "\n\n"); + } + + var wdpEx = ex as DevicePortalException; + if (wdpEx != null) + { + errorMessage.Append("DevicePortal exception details: \n"); + errorMessage.Append("RequestURI: " + wdpEx.RequestUri + "\n"); + errorMessage.Append("Reason: " + wdpEx.Reason + "\n"); + errorMessage.Append("HTTP Status: " + wdpEx.StatusCode.ToString() + "\n"); + errorMessage.Append("\n"); + } + + Console.Out.WriteLine(errorMessage.ToString()); + throw; + } + finally + { + _runningOperation.Release(); + } + } + + private void OutputOperationSpecificUsageText(Operation op) + { + switch (op) + { + + case Operation.ListInstalledApps: + Console.Out.WriteLine(ListAppsOpUsageTExt); + break; + + case Operation.Install: + Console.Out.WriteLine(InstallOpUsageText); + break; + + case Operation.Run: + Console.Out.WriteLine(RunOpUsageText); + break; + + case Operation.Uninstall: + Console.Out.WriteLine(UninstallOpUsageText); + break; + + default: + OutputDefaultUsageText(); + break; + } + } + + private void OutputDefaultUsageText() + { + string errorMessage = ""; + + if (!_helpFlag) + { + errorMessage = "Invalid App operation\n"; + } + + Console.Out.WriteLine(errorMessage); + Console.Out.WriteLine(AppOperationUsageText); + } + + private void ExecuteListInstalledAppsOperation(ParameterHelper parameters) + { + Task packagesTask = _portal.GetInstalledAppPackagesAsync(); + packagesTask.Wait(); + + var packages = packagesTask.Result; + Console.Out.WriteLine(packages.ToString()); + } + + private void ExecuteInstallOperation(ParameterHelper parameters) + { + // Parse app and dependency filenames from the parameters + string appxFile = parameters.GetParameterValue(ParameterAppx); + string certificate = parameters.GetParameterValue(ParameterCert); + + _portal.AppInstallStatus += OnAppInstallStatus; + _lastInstallStatus = null; + try + { + if (!String.IsNullOrWhiteSpace(appxFile)) + { + ExecuteInstallAppx(parameters, appxFile, certificate); + } + else + { + throw new System.ArgumentNullException("Must specify an appx file to install"); + } + } + finally + { + _portal.AppInstallStatus -= OnAppInstallStatus; + _lastInstallStatus = null; + } + } + + private void ExecuteInstallAppx(ParameterHelper parameters, string appxFile, string certificate) + { + if (_verbose) + { + Console.Out.WriteLine("Starting Appx installation..."); + } + + var file = new FileInfo(Path.GetFullPath(appxFile)); + if (!file.Exists) + { + throw new System.IO.FileNotFoundException("Specified appx file '" + appxFile + "' wasn't found"); + } + + if (!String.IsNullOrWhiteSpace(certificate)) + { + var certFile = new FileInfo(certificate); + if (!certFile.Exists) + { + throw new System.IO.FileNotFoundException("Specified certificate file '" + certFile + "' wasn't found"); + } + certificate = certFile.FullName; + } + else + { + // Must pass in null instead of empty string if certificate is omitted + certificate = null; + } + + // Parse the AppxManfest contained in the Appx file to extract the package name, dependencies, AppID, etc. + AppxManifest appxData; + try + { + appxData = AppxManifest.Get(appxFile); + } + catch (Exception ex) + { + Console.Out.WriteLine("Failed to parse Appx manifest: " + ex.Message); + throw; + } + if (!appxData.IsValid) + { + throw new System.ArgumentException("Specified Appx '" + appxFile + "' contains an invalid AppxManifest"); + } + + // Construct an "identity" object from the AppxManifest data which can be referenced later to launch the installed app + var appIdentity = new AppPackageIdentity(appxData); + + // Query for app packages already installed on the device and uninstall them if necessary + // NOTE: Check for any uninstall any package matching this appx PackageName and Publisher to + // ensure a clean install of the new build + + List matchingPackages; + if (TryRetrieveInstalledPackages(appIdentity, out matchingPackages) && matchingPackages.Count() > 0) + { + if (_verbose) + { + Console.Out.WriteLine("Uninstalling previous app.."); + } + + foreach (var package in matchingPackages) + { + try + { + if (_verbose) + { + Console.Out.WriteLine("Uninstalling package: " + package.FullName); + } + Task uninstallTask = _portal.UninstallApplicationAsync(package.FullName); + } + catch (AggregateException ex) + { + // NOTE: We really shouldn't continue with installation if we failed to remove a previous version of the app. + // If a version of the app remains on the device, the Install API will NOT replace it but still reports "success", + // meaning the user could be running old code and not know it. A hard fail is the only way to ensure this doesn't happen. + Console.Out.WriteLine("Uninstall of package '" + package.FullName + "' failed: " + ex.InnerException.Message); + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + } + } + + if (_verbose) + { + Console.Out.WriteLine("Finished uninstalling previous app packages"); + } + } + + Task installTask = _portal.InstallApplicationAsync(null, file.FullName, appxData.Dependencies, certificate, 500, 1, false); + try + { + installTask.Wait(); + Console.Out.WriteLine("Installation completed successfully"); + } + catch (AggregateException ex) + { + Console.Out.WriteLine("Installation of Appx failed!"); + + HandleInstallOperationException(ex); + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + } + + // Save AppIdentity to field after successful installation + _installedAppId = appIdentity; + + // If the app Identity is "complete" we have the FullPackageName and AUMID parameters, so we'll + // add them to our parameter set to later launch the app; no need to query package info from the device + if (_installedAppId.CompleteIdentity) + { + parameters.AddOrUpdateParameter(parameterPackage, _installedAppId.PackageFullName); + parameters.AddOrUpdateParameter(ParameterAumid, _installedAppId.LaunchId); + } + } + + private void HandleInstallOperationException(AggregateException ex) + { + if (ex == null) return; + + // If available, log the last status update before the exception + if (_lastInstallStatus != null) + { + string errorMessage; + errorMessage = String.Format("Installation failed in phase {0} - last status: {1}", _lastInstallStatus.Phase, _lastInstallStatus.Message); + if (_verbose) + { + Console.Out.WriteLine(errorMessage); + } + } + + // If multiple exception were encountered we want to log each of them + // The "main" exception will be handled by the top-level ExecuteOperation method + if (ex.InnerExceptions.Count > 1) + { + var sb = new StringBuilder(); + sb.Append("Multiple exception were thrown during operation:\n"); + + foreach (var item in ex.InnerExceptions) + { + sb.Append(" " + item.Message + "\n"); + } + + if (_verbose) + { + Console.Out.WriteLine(sb.ToString()); + } + } + } + + private void ExecuteRunOperation(ParameterHelper parameters) + { + string packageName = parameters.GetParameterValue(parameterPackage); + string launchId = parameters.GetParameterValue(ParameterAumid); + + // These parameters are required unless we just performed and install operation + if (String.IsNullOrWhiteSpace(packageName) && _installedAppId == null) + { + throw new System.ArgumentException("Must provide full name of app package to launch"); + } + if (String.IsNullOrWhiteSpace(launchId) && _installedAppId == null) + { + throw new System.ArgumentException("Must provide the AUMID of the app to launch from the specified package"); + } + + // Just installed an app but unable to retrieve package's full name and/or family name + // So we'll try to query the values from the remote device + if (_installedAppId != null && !_installedAppId.CompleteIdentity) + { + + if (!TryRetrievePackageNameAndLaunchIdFromDevice(_installedAppId, 4, out packageName, out launchId)) + { + throw new System.Exception("Failed to retrieve necessary app package info from the remote device; cannot launch app"); + } + } + else if (_installedAppId != null) + { + // If we just installed the app, must wait until it's fully installed/registered with the OS, otherwise launch operation will fail + WaitUnilAppIsFullyInstalled(packageName); + } + + Task launchTask = _portal.LaunchApplicationAsync(launchId, packageName); + try + { + launchTask.Wait(); + + Console.WriteLine("Application launched"); + } + catch (AggregateException ex) + { + Console.Out.WriteLine("App launch failed!"); + + HandleInstallOperationException(ex); + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + } + } + + private void ExecuteUninstallyOperation(ParameterHelper parameters) + { + string package = parameters.GetParameterValue(parameterPackage); + + if (String.IsNullOrWhiteSpace(package)) + { + throw new System.ArgumentException("Must provide full name of app package to uninstall"); + } + + Task installTask = _portal.UninstallApplicationAsync(package); + try + { + installTask.Wait(); + Console.Out.WriteLine("Uninstall completed successfully"); + } + catch (AggregateException ex) + { + Console.Out.WriteLine("Uninstall of app failed!"); + + HandleInstallOperationException(ex); + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + } + } + + private void OnAppInstallStatus(object sender, ApplicationInstallStatusEventArgs args) + { + if (_verbose) + { + Console.Out.WriteLine(args.Message); + } + _lastInstallStatus = args; + } + + private bool TryRetrievePackageNameAndLaunchIdFromDevice(AppPackageIdentity packageId, int numAttempts, out string packageFullName, out string launchId) + { + // Already have the info locally and don't need to query from device + if (packageId.CompleteIdentity) + { + packageFullName = packageId.PackageFullName; + launchId = packageId.LaunchId; + return true; + } + + if (_verbose) + { + Console.Out.WriteLine("Attempting to query PackageFullName and AUMID from remote device"); + } + + packageFullName = String.Empty; + launchId = String.Empty; + bool successful = false; + + while (numAttempts > 0 && !successful) + { + numAttempts--; + + try + { + Task packagesTask = _portal.GetInstalledAppPackagesAsync(); + packagesTask.Wait(); + + // This basic query should provide the matching package in most cases + // -PackageName must exactly match + // -Publisher must exactly match + // -AUMID (called "AppId" in PackageInfo class) must contain our AppId value (registered app entry point) + // -Version string must exactly match + var matchingPackages = + (from package in packagesTask.Result.Packages + where + package.Name == packageId.PackageName && + package.Publisher == package.Publisher && + package.AppId.Contains(package.AppId) && // AppId from PackageInfo is actually full AUMID + package.Version.ToString() == packageId.Version + select package).ToList(); + + PackageInfo matchingPackage = null; + + if (matchingPackages.Count() == 0) + { + if (_verbose) + { + Console.Out.Write("Failed to find '" + packageId.PackageName + "' package installed on the device..."); + Console.Out.WriteLine((numAttempts > 0) ? "trying again" : "giving up"); + } + } + else if (matchingPackages.Count() > 1) + { + // It's technically possible for the above query to return multiple packages, in which case we need to + // disambiguate using the optional package identifiers (CPU architecture and ResourceId). + // Since PackageInfo doesn't provide this fields directly, need to split PackageFullName + // into component its component parts => + // [0] PackageName + // [1] Version + // [2] CPU architecture + // [3] ResourceId (if present) + // [4] Publisher name hash + + foreach (var package in matchingPackages) + { + var nameParts = package.FullName.Split(new char[] { '_' }, 5); + if (nameParts.Length < 5) continue; + + // ResoruceId will be an empty string if not present, which should match our manifest data + if (nameParts[2] == packageId.CpuArchitecture && nameParts[3] == packageId.ResourceId) + { + matchingPackage = package; + break; + } + } + } + else matchingPackage = matchingPackages.First(); + + if (matchingPackage != null) + { + packageFullName = matchingPackage.FullName; + launchId = matchingPackage.AppId; + successful = true; + } + + // Query attempt may failed because it a few seconds before newly installed apps are reported + // So if we have more attempts then wait a bit before trying again + if (numAttempts > 0 && !successful) + { + Thread.Sleep(2000); + } + } + catch (Exception ex) + { + if (_verbose) + { + Console.Out.WriteLine("Failed to acquire list of installed apps from device: " + ex.Message); + } + } + } + + if (_verbose) + { + if (!successful) + { + Console.Out.WriteLine("Failed to retrieve FullPackageName and AUMID from remote device"); + } + else Console.Out.WriteLine("Successfully retrieved FullPackageName and AUMID from remote device"); + } + + return successful; + } + + private bool TryRetrieveInstalledPackages(AppPackageIdentity packageId, out List matchingPackages) + { + if (_verbose) + { + Console.Out.WriteLine("Attempting to query installed packages matching app"); + } + + matchingPackages = null; + bool successful = false; + + try + { + Task packagesTask = _portal.GetInstalledAppPackagesAsync(); + packagesTask.Wait(); + + // We want to find all packages that *loosely* match our appx in case something like + // architecture or configuration changed + matchingPackages = + (from package in packagesTask.Result.Packages + where + package.Name == packageId.PackageName && + package.Publisher == package.Publisher + select package).ToList(); + + successful = true; + } + catch (Exception ex) + { + if (_verbose) + { + Console.Out.WriteLine("Failed to acquire list of installed apps from device: " + ex.Message); + } + } + + if (_verbose) + { + if (!successful) + { + Console.Out.WriteLine("Failed to retrieve installed packages from the device"); + } + else Console.Out.WriteLine("Successfully retrieved installed packages matching app from the device"); + } + + return successful; + } + + private void WaitUnilAppIsFullyInstalled(string fullPackageName) + { + PackageInfo appPackage = null; + int numAttempts = 5; + + // DevicePortal API has an annoying quirk in which the "install" call will return before the app is fully ready + // on the remote device. It takes a few extra seconds before Windows can launch it, and attempting to launch the app before + // it's ready results in an error. So, wait until we can successfully query the package from the device using the "list" + // operation, once we see it in the returned results we know the app is ready and can be launched. + + do + { + Thread.Sleep(3000); + + try + { + Task packagesTask = _portal.GetInstalledAppPackagesAsync(); + packagesTask.Wait(); + + appPackage = packagesTask.Result.Packages.FirstOrDefault(package => (package.FullName == fullPackageName)); + } + catch {; } + + numAttempts--; + + } while (appPackage == null && numAttempts > 0); + } + } +} diff --git a/Source/AppxManifest.cs b/Source/AppxManifest.cs new file mode 100644 index 0000000..de27b9e --- /dev/null +++ b/Source/AppxManifest.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Xml.Linq; + +namespace DevicePortalTool +{ + public class Dependency + { + public string Name { get; private set; } + public Version MinVersion { get; private set; } + + public Dependency(string name, string minVersion) + { + Name = name; + MinVersion = new Version(minVersion); + } + } + + public class AppxManifest + { + private static Dictionary ManifestCache = new Dictionary(); + + public string AppxPath { get; private set; } + public Guid PhoneProductId { get; private set; } + public string PackageName { get; private set; } + public string Publisher { get; private set; } + public string Version { get; private set; } + public string AppId { get; private set; } + public List Dependencies { get; private set; } + public string CpuArchitecture { get; private set; } + public string ResourceId { get; private set; } + public bool IsFramework { get; private set; } + public bool IsDependency { get; private set; } + public bool IsValid { get; private set; } + + public static AppxManifest Get(string appxPath) + { + if (!ManifestCache.ContainsKey(appxPath)) + { + return new AppxManifest(appxPath); // Constructor will add it to manifest cache midway through construction. + } + + return ManifestCache[appxPath]; + } + + public static AppxManifest Get(string appxPath, string extractedPath) + { + if (!ManifestCache.ContainsKey(appxPath)) + { + return new AppxManifest(appxPath, extractedPath); // Constructor will add it to manifest cache midway through construction. + } + + return ManifestCache[appxPath]; + } + + private AppxManifest(string appxPath) + { + AppxPath = appxPath; + var extension = Path.GetExtension(appxPath); + if (string.Equals(extension, ".xml", StringComparison.OrdinalIgnoreCase)) + { + Parse(XDocument.Load(appxPath)); + } + else + { + using (var archive = System.IO.Compression.ZipFile.OpenRead(appxPath)) + { + var manifestStream = archive.GetEntry("AppxManifest.xml").Open(); + Parse(XDocument.Load(manifestStream)); + } + } + } + + private AppxManifest(string appxPath, string extractedPath) + { + AppxPath = appxPath; + using (var stream = new StreamReader(Path.Combine(extractedPath, "AppXManifest.xml"))) + { + Parse(XDocument.Load(stream)); + } + } + + private void Parse(XDocument manifest) + { + ManifestCache.Add(AppxPath, this); + + string namezspace = "http://schemas.microsoft.com/appx/manifest/foundation/windows10"; + try + { + var phoneIdentity = manifest.Root.Element(XName.Get("PhoneIdentity", "http://schemas.microsoft.com/appx/2014/phone/manifest")); + if (phoneIdentity != null) + { + PhoneProductId = Guid.Parse(phoneIdentity.Attribute("PhoneProductId").Value); + } + + var identity = manifest.Root.Element(XName.Get("Identity", namezspace)); + if (identity == null) + { + throw new ArgumentNullException("identity"); + } + + PackageName = identity.Attribute("Name")?.Value; + if (PackageName == null) + { + throw new ArgumentNullException("packageName"); + } + + Publisher = identity.Attribute("Publisher")?.Value; + if (Publisher == null) + { + throw new ArgumentNullException("publisher"); + } + + Version = identity.Attribute("Version")?.Value; + if (Version == null) + { + throw new ArgumentNullException("version"); + } + + var applications = manifest.Root.Element(XName.Get("Applications", namezspace)); + if (applications != null) + { + var applicationElement = applications.Element(XName.Get("Application", namezspace)); + + if (applicationElement != null) + { + AppId = applicationElement.Attribute(XName.Get("Id"))?.Value; + if (AppId == null) + { + throw new ArgumentNullException("appid"); + } + } + } + + // Optional identity attributes + CpuArchitecture = identity.Attribute("ProcessorArchitecture")?.Value ?? "neutral"; + ResourceId = identity.Attribute("ResourceId")?.Value ?? ""; + + var properties = manifest.Root.Element(XName.Get("Properties", namezspace)); + var frameworkAttribute = properties.Element(XName.Get("Framework", namezspace)); + + IsFramework = frameworkAttribute != null && frameworkAttribute.Value.Equals("True", StringComparison.InvariantCultureIgnoreCase); + } + catch // This will happen if we happen to try to parse non-UWP app manifest + { + IsValid = false; + return; + } + + IsValid = true; + + var dependencies = manifest.Root.Element(XName.Get("Dependencies", namezspace)); + + Dependencies = new List(); + if (dependencies != null) + { + foreach (var dependencyInfo in dependencies.Descendants()) + { + var dependency = new Dependency(dependencyInfo.Attribute("Name").Value, dependencyInfo.Attribute("MinVersion").Value); + AddDependency(dependency, AppxPath); + } + } + } + + private void AddDependency(Dependency dependency, string appxPath) + { + var appxFolder = Path.GetDirectoryName(appxPath); + var dependenciesFolder = Path.Combine(appxFolder, "Dependencies"); + var searchFolders = new string[] { Path.Combine(dependenciesFolder, CpuArchitecture), dependenciesFolder, appxFolder }; + + foreach (var searchFolder in searchFolders) + { + if (!Directory.Exists(searchFolder)) + continue; + + foreach (var file in Directory.GetFiles(searchFolder, "*.appx")) + { + if (AddDependenciesIfMatches(file, dependency)) + { + return; + } + } + } + } + + private bool AddDependenciesIfMatches(string path, Dependency dependency) + { + var dependencyManifest = Get(path); + + if (dependencyManifest.IsValid && dependencyManifest.PackageName.Equals(dependency.Name, StringComparison.InvariantCultureIgnoreCase)) + { + Dependencies.AddRange(dependencyManifest.Dependencies); + Dependencies.Add(path); + dependencyManifest.IsDependency = true; + return true; + } + + return false; + } + } +} diff --git a/Source/PackageHelper.cs b/Source/PackageHelper.cs new file mode 100644 index 0000000..0b631f7 --- /dev/null +++ b/Source/PackageHelper.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace DevicePortalTool +{ + public class AppPackageIdentity + { + public string PackageName { get; private set; } + public string AppId { get; private set; } + public string Publisher { get; private set; } + public string Version { get; private set; } + public Version VersionValue { get; private set; } + public string CpuArchitecture { get; private set; } + public UInt32 CpuArchitectureValue { get; private set; } + public string ResourceId { get; private set; } + public string PackageFullName { get; private set; } + public string PackageFamilyName { get; private set; } + public string LaunchId { get; private set; } + public bool CompleteIdentity { get; private set; } + + public AppPackageIdentity(AppxManifest appxData) + { + InitializeInternal(appxData.PackageName, appxData.AppId, appxData.Publisher, appxData.Version, appxData.CpuArchitecture, appxData.ResourceId); + } + + public AppPackageIdentity(string packageName, string appId, string publisher, string version) + { + InitializeInternal(packageName, appId, publisher, version, "neutral", ""); + } + + public AppPackageIdentity(string packageName, string appId, string publisher, string version, string cpuArchitecture, string resourceId) + { + InitializeInternal(packageName, appId, publisher, version, cpuArchitecture, resourceId); + } + + private void InitializeInternal(string packageName, string appId, string publisher, string version, string cpuArchitecture, string resourceId) + { + if (String.IsNullOrWhiteSpace(packageName)) + throw new System.ArgumentNullException("PackageName is invalid"); + if (String.IsNullOrWhiteSpace(appId)) + throw new System.ArgumentNullException("AppId is invalid"); + if (String.IsNullOrWhiteSpace(publisher)) + throw new System.ArgumentNullException("Publisher is invalid"); + if (String.IsNullOrWhiteSpace(version)) + throw new System.ArgumentNullException("Version is invalid"); + + if (cpuArchitecture == null) + cpuArchitecture = "neutral"; + if (resourceId == null) + resourceId = ""; + + this.PackageName = packageName; + this.AppId = appId; + this.Publisher = publisher; + this.Version = version; + this.CpuArchitecture = cpuArchitecture; + this.ResourceId = resourceId; + + System.Version verObj; + if (System.Version.TryParse(version, out verObj)) + { + this.VersionValue = verObj; + } + + this.CpuArchitectureValue = PackageHelper.ProcessorArchitectureStringToEnum(cpuArchitecture); + + this.PackageFamilyName = PackageHelper.TryGetPackageFamilyName(packageName, publisher); + this.PackageFullName = PackageHelper.TryGetPackageFullName(packageName, publisher, this.VersionValue, this.CpuArchitectureValue); + + // If PackageFamilyName was successfully retrieved, construct the "LaunchId" (AUMID) + if (!String.IsNullOrWhiteSpace(this.PackageFamilyName)) + { + this.LaunchId = this.PackageFamilyName + "!" + this.AppId; + } + else this.LaunchId = String.Empty; + + // If we successfully retrieve FamilyName and FullName then we have package identity is "complete" + // Otherwise one or more properties are missing or invalid + if (!String.IsNullOrWhiteSpace(this.PackageFullName) && !String.IsNullOrWhiteSpace(this.PackageFamilyName)) + { + this.CompleteIdentity = true; + } + } + } + + internal class PackageHelper + { + public static UInt32 ProcessorArchitectureStringToEnum(string architecture) + { + architecture = architecture.ToLowerInvariant(); + switch (architecture) + { + case "x86": return (UInt32)APPX_PACKAGE_ARCHITECTURE.APPX_PACKAGE_ARCHITECTURE_X86; + case "x64": return (UInt32)APPX_PACKAGE_ARCHITECTURE.APPX_PACKAGE_ARCHITECTURE_X64; + case "arm": return (UInt32)APPX_PACKAGE_ARCHITECTURE.APPX_PACKAGE_ARCHITECTURE_ARM; + case "arm64": return (UInt32)APPX_PACKAGE_ARCHITECTURE.APPX_PACKAGE_ARCHITECTURE_ARM64; + } + + return (UInt32)APPX_PACKAGE_ARCHITECTURE.APPX_PACKAGE_ARCHITECTURE_NEUTRAL; + } + + public static string TryGetPackageFamilyName(string name, string publisherId) + { + string packageFamilyName = String.Empty; + + try + { + var packageId = new PACKAGE_ID + { + name = name, + publisher = publisherId, + }; + + uint packageFamilyNameLength = 0; + + // First get the length of the Package Name -> Pass NULL as Output Buffer + if (PackageFamilyNameFromId(packageId, ref packageFamilyNameLength, null) == 122) // ERROR_INSUFFICIENT_BUFFER + { + var packageFamilyNameBuilder = new StringBuilder((int)packageFamilyNameLength); + if (PackageFamilyNameFromId(packageId, ref packageFamilyNameLength, packageFamilyNameBuilder) == 0) + { + packageFamilyName = packageFamilyNameBuilder.ToString(); + } + } + } + catch {; } + + return packageFamilyName; + } + + public static string TryGetPackageFullName(string name, string publisherId, Version appVersion, UInt32 cpuArchitecture) + { + string packageFullName = String.Empty; + + try + { + var major = Convert.ToUInt16(appVersion.Major); + var minor = Convert.ToUInt16(appVersion.Minor); + var build = Convert.ToUInt16(appVersion.Build); + var rev = Convert.ToUInt16(appVersion.Revision); + + UInt64 packedVersion = + Convert.ToUInt64(rev) << 0 | + Convert.ToUInt64(build) << 16 | + Convert.ToUInt64(minor) << 32 | + Convert.ToUInt64(major) << 48; + + + var packageId = new PACKAGE_ID + { + name = name, + publisher = publisherId, + processorArchitecture = cpuArchitecture, + version = packedVersion + }; + + uint packageFullNameLength = 0; + + // First get the length of the Package Name -> Pass NULL as Output Buffer + if (PackageFullNameFromId(packageId, ref packageFullNameLength, null) == 122) // ERROR_INSUFFICIENT_BUFFER + { + var packageFullNameBuilder = new StringBuilder((int)packageFullNameLength); + if (PackageFullNameFromId(packageId, ref packageFullNameLength, packageFullNameBuilder) == 0) + { + packageFullName = packageFullNameBuilder.ToString(); + } + } + } + catch {; } + + return packageFullName; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 4)] + internal class PACKAGE_ID + { + public UInt32 reserved; + public UInt32 processorArchitecture; + public UInt64 version; + public string name; + public string publisher; + public string resourceId; + public string publisherId; + }; + + enum APPX_PACKAGE_ARCHITECTURE + { + APPX_PACKAGE_ARCHITECTURE_X86 = 0, + APPX_PACKAGE_ARCHITECTURE_ARM = 5, + APPX_PACKAGE_ARCHITECTURE_X64 = 9, + APPX_PACKAGE_ARCHITECTURE_NEUTRAL = 11, + APPX_PACKAGE_ARCHITECTURE_ARM64 = 12 + }; + + // NOTE: These APIs are only available in Windows 8 and later, and If called on Windows 7 we expect an EntryPointNotFoundException will be thrown. + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] + private static extern uint PackageFamilyNameFromId(PACKAGE_ID packageId, ref uint packageFullNameLength, StringBuilder packageFullName); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] + private static extern uint PackageFullNameFromId(PACKAGE_ID packageId, ref uint packageFullNameLength, StringBuilder packageFullName); + } +} diff --git a/Source/ParameterHelper.cs b/Source/ParameterHelper.cs new file mode 100644 index 0000000..eb06866 --- /dev/null +++ b/Source/ParameterHelper.cs @@ -0,0 +1,123 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +// +// Modified under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; + +namespace DevicePortalTool +{ + public class ParameterHelper + { + public static readonly string HelpFlag = "?"; + public static readonly string VerboseFlag = "v"; + public static readonly string Operation = "op"; + public static readonly string DeviceIpAddress = "ip"; + public static readonly string WdpUser = "user"; + public static readonly string WdpPassword = "pwd"; + public static readonly string StdinCredentials = "stdincred"; + + public OpperationArea Area { get; private set; } = OpperationArea.None; + + private Dictionary parameters = new Dictionary(); + private List flags = new List(); + + public void AddParameter(string name, string value) + { + this.parameters.Add(name, value); + } + + public void AddOrUpdateParameter(string name, string value) + { + if (this.parameters.ContainsKey(name)) + { + this.parameters[name] = value; + } + else this.parameters.Add(name, value); + } + + public string GetParameterValue(string key) + { + if (this.parameters.ContainsKey(key)) + { + return this.parameters[key]; + } + else + { + return String.Empty; + } + } + + public bool HasParameter(string key) + { + return this.parameters.ContainsKey(key); + } + + public bool HasFlag(string flag) + { + return this.flags.Contains(flag); + } + + public void ParseCommandLine(string[] args) + { + // If nothing specified then add "help" flag to display usage + if (args.Length == 0) + { + this.flags.Add(ParameterHelper.HelpFlag); + return; + } + + // The operation area must always be the 1st parameter + // NOTE: In C# args[0] is program name + Area = Program.OperationAreaStringToEnum(args[0]); + + // Parse the command line args + for (int i = 0; i < args.Length; ++i) + { + string arg = args[i]; + if (!arg.StartsWith("/") && !arg.StartsWith("-")) + { + // We expect the first parameter to be the "area" which isn't prefixed with a slash + if (i == 0) continue; + + throw new Exception(string.Format("Unrecognized argument: {0}", arg)); + } + + arg = arg.Substring(1); + + int valueIndex = arg.IndexOf(':'); + string value = null; + + // If this contains a colon, separate it into the param and value. Otherwise add it as a flag + if (valueIndex > 0) + { + value = arg.Substring(valueIndex + 1); + arg = arg.Substring(0, valueIndex); + + this.parameters.Add(arg.ToLowerInvariant(), value); + } + else + { + this.flags.Add(arg.ToLowerInvariant()); + } + } + } + + public static string EncodeBase64(string plainText) + { + var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText); + return Convert.ToBase64String(plainTextBytes); + } + + public static string DecodeBase64(string base64Text) + { + var base64EncodedBytes = System.Convert.FromBase64String(base64Text); + return System.Text.Encoding.UTF8.GetString(base64EncodedBytes); + } + } +} diff --git a/Source/Program.cs b/Source/Program.cs new file mode 100644 index 0000000..5ebbcc2 --- /dev/null +++ b/Source/Program.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.Tools.WindowsDevicePortal; + +namespace DevicePortalTool +{ + public enum OpperationArea + { + None, + Application, + }; + + public enum ProgramErrorCodes : int + { + Success = 0, + SuccessHelp = 1, + OperationFailed = -1, + InvalidParameters = -2, + AuthenticationFailed = -3, + ConnectionFailed = -4, + UnexpectedError = -5, + }; + + class Program + { + private static readonly string GeneralUsageMessage = + "Executes a Windows DevicePortal (WDP) operation on a remote Windows 10 device\n" + + "\n" + + "Usage:\n" + + "DevicePortalTool /op: [operation parameters]] /ip: [/stdincred | /user:WDP username /pwd: - must be one of the following values:\n" + + " app - execute an Application related operation\n" + + " /op - area specific operation value with additional parameters\n" + + " /ip - IP address of remote device to operate on\n" + + " /stdincred - WDP credentials are read from stdin stream instead of command line (no prompts)\n" + + " credentials read as two separate lines of Base64 encoded strings (user name then password)\n" + + " /user - Name of WDP user on the device\n" + + " /pwd - Password of WDP user on the device\n" + + " /v - Verbose logging output\n" + + "\n" + + "DevicePortalTool /?\n" + + " Display area specific usage\n" + + "\n" + + "DevicePortalTool /?\n" + + " Display this usage\n" + ; + + public static OpperationArea OperationAreaStringToEnum(string areaName) + { + if (String.IsNullOrWhiteSpace(areaName)) + { + return OpperationArea.None; + } + if (areaName.Equals("app", StringComparison.OrdinalIgnoreCase)) + { + return OpperationArea.Application; + } + + return OpperationArea.None; + } + + public static int Main(string[] args) + { + var resultCode = ExecuteMain(args); + + Console.Out.WriteLine("ExitCode: " + (int)resultCode + " (" + resultCode.ToString() + ")"); + return (int)resultCode; + } + + private static ProgramErrorCodes ExecuteMain(string[] args) + { + ParameterHelper parameters; + ProgramErrorCodes errorCode; + Uri targetDevice = null; + bool helpFlag; + bool verbose; + + errorCode = ParseParametersAndPerformBasicValidation(args, out parameters, out targetDevice, out verbose, out helpFlag); + if (errorCode != ProgramErrorCodes.Success) + return errorCode; + + if (!helpFlag && parameters.HasFlag(ParameterHelper.StdinCredentials)) + { + try + { + string username; + string password; + + if (!TryReadCredentialsFromStdin(out username, out password)) + { + Console.Out.WriteLine("Failed to read WDP credentials from stdin"); + Console.Out.WriteLine(); + + return ProgramErrorCodes.InvalidParameters; + } + + parameters.AddOrUpdateParameter(ParameterHelper.WdpUser, username); + parameters.AddOrUpdateParameter(ParameterHelper.WdpPassword, password); + } + catch (Exception ex) + { + Console.Out.WriteLine("Fatal error reading WDP credentials from stdin: " + ex.Message); + Console.Out.WriteLine(); + + return ProgramErrorCodes.UnexpectedError; + } + } + + DevicePortal portal; + if (!helpFlag) + { + try + { + if (!TryOpenDevicePortalConnection(targetDevice, parameters, out portal)) + { + if (portal != null && portal.ConnectionHttpStatusCode == System.Net.HttpStatusCode.Unauthorized) + { + Console.Out.WriteLine("Aborting due to failed authentication"); + Console.Out.WriteLine(); + return ProgramErrorCodes.AuthenticationFailed; + } + else + { + Console.Out.WriteLine("Aborting due to failed connection with remote device"); + Console.Out.WriteLine(); + return ProgramErrorCodes.ConnectionFailed; + } + } + } + catch (Exception ex) + { + Console.Out.WriteLine("Fatal error initializing DevicePortal: " + ex.Message); + Console.Out.WriteLine(); + + return ProgramErrorCodes.UnexpectedError; + } + } + else + { + // Need to create a dummy DevicePortal so can safely call into area-specific operation processor + // The processor can then output operation specific help info + portal = new DevicePortal(new DefaultDevicePortalConnection("http://0.0.0.0", "", "")); + } + + var operationResult = ProgramErrorCodes.Success; + switch (parameters.Area) + { + case OpperationArea.Application: + if (!AppOperation.TryExecuteApplicationOperation(portal, parameters)) + { + operationResult = ProgramErrorCodes.OperationFailed; + } + break; + + default: + // This case should have already been handled by a parameter check above + operationResult = ProgramErrorCodes.InvalidParameters; + break; + } + + // If successful but help switch was set return a different resultCode + if (operationResult == ProgramErrorCodes.Success && helpFlag) + { + operationResult = ProgramErrorCodes.SuccessHelp; + } + + // If a debugger is attached, don't close but instead loop here until + // closed. + while (System.Diagnostics.Debugger.IsAttached) + { + System.Threading.Thread.Sleep(0); + } + + return operationResult; + } + + private static ProgramErrorCodes ParseParametersAndPerformBasicValidation(string[] args, out ParameterHelper parameters, out Uri targetDevice, out bool verbose, out bool helpFlag) + { + parameters = new ParameterHelper(); + targetDevice = null; + verbose = false; + helpFlag = false; + + try + { + parameters.ParseCommandLine(args); + } + catch (Exception ex) + { + Console.Out.WriteLine("Fatal error parsing command arguments: " + ex.Message); + Console.Out.WriteLine(); + + return ProgramErrorCodes.UnexpectedError; + } + + verbose = parameters.HasFlag(ParameterHelper.VerboseFlag); + helpFlag = parameters.HasFlag(ParameterHelper.HelpFlag); + + if (parameters.Area == OpperationArea.None) + { + if (helpFlag) + { + Console.WriteLine(GeneralUsageMessage); + Console.WriteLine(); + } + else + { + Console.Out.WriteLine("Invalid parameters: Must specify a valid operation area as the first parameter"); + Console.Out.WriteLine(); + } + + return ProgramErrorCodes.InvalidParameters; + } + + if (!helpFlag) + { + string address = parameters.GetParameterValue(ParameterHelper.DeviceIpAddress); + string invalidReason = null; + + if (String.IsNullOrWhiteSpace(address)) + { + invalidReason = "Must specify IP address of remote Windows Device Portal with /ip switch"; + } + else if (!Uri.TryCreate(address, UriKind.Absolute, out targetDevice)) + { + invalidReason = "isn't a proper URI"; + } + else if (targetDevice.Scheme != "http" && targetDevice.Scheme != "https") + { + invalidReason = "must specify http or https scheme"; + } + else if (targetDevice.IsDefaultPort) + { + invalidReason = "doesn't specify a WDP port number"; + } + + if (invalidReason != null) + { + Console.Out.WriteLine("Invalid parameters: IP address '" + address + "'; " + invalidReason); + Console.Out.WriteLine("IP address must be in the following format: http(s)://:"); + Console.Out.WriteLine("The correct address string can be found under 'Developer' settings on the host device"); + Console.Out.WriteLine(); + return ProgramErrorCodes.InvalidParameters; + } + } + + if (!helpFlag && parameters.HasFlag(ParameterHelper.StdinCredentials)) + { + if (parameters.HasParameter(ParameterHelper.WdpUser) || parameters.HasParameter(ParameterHelper.WdpPassword)) + { + Console.Out.WriteLine("Invalid parameters: Cannot pass WDP credentials on command line (/user /pwd) when using /stdincred switch; use one method or the other"); + Console.Out.WriteLine(); + return ProgramErrorCodes.InvalidParameters; + } + } + + return ProgramErrorCodes.Success; + } + + private static bool TryReadCredentialsFromStdin(out string userName, out string password) + { + userName = String.Empty; + password = String.Empty; + + // We expect username/password (base64 encoded) to be passed "piped" in from a calling process + // So there's no prompt and we'll fail if don't read anything after a few seconds + { + var readTask = Console.In.ReadLineAsync(); + if (!readTask.Wait(5000)) + return false; + + userName = ParameterHelper.DecodeBase64(readTask.Result); + } + + { + var readTask = Console.In.ReadLineAsync(); + if (!readTask.Wait(5000)) + return false; + + password = ParameterHelper.DecodeBase64(readTask.Result); + } + + return true; + } + + private static bool TryOpenDevicePortalConnection(Uri targetDevice, ParameterHelper parameters, out DevicePortal portal) + { + string userName = parameters.GetParameterValue(ParameterHelper.WdpUser); + string password = parameters.GetParameterValue(ParameterHelper.WdpPassword); + + bool success = true; + portal = new DevicePortal(new DefaultDevicePortalConnection(targetDevice.ToString(), userName, password)); + try + { + // We need to handle this event otherwise remote connection will be rejected if + // device isn't trusted by local PC + portal.UnvalidatedCert += DoCertValidation; + + var connectTask = portal.ConnectAsync(updateConnection: false); + connectTask.Wait(); + + if (portal.ConnectionHttpStatusCode != System.Net.HttpStatusCode.OK) + { + if (portal.ConnectionHttpStatusCode == System.Net.HttpStatusCode.Unauthorized) + { + throw new System.UnauthorizedAccessException("Connection rejected due to missing/incorrect credentials; specify valid credentials with /user and /pwd switches"); + } + else if (!string.IsNullOrEmpty(portal.ConnectionFailedDescription)) + { + throw new System.OperationCanceledException(string.Format("WDP connection failed (HTTP {0}) : {1}", (int)portal.ConnectionHttpStatusCode, portal.ConnectionFailedDescription)); + } + else + { + throw new System.OperationCanceledException(string.Format("WDP connection failed (HTTP {0}) : no additional information", (int)portal.ConnectionHttpStatusCode)); + } + } + } + catch (Exception ex) + { + bool verbose = parameters.HasFlag(ParameterHelper.VerboseFlag); + Console.Out.WriteLine("Failed to open DevicePortal connection to '" + portal.Address + "'\n" + (verbose ? ex.ToString() : ex.Message)); + + success = false; + } + + return success; + } + + private static bool DoCertValidation(DevicePortal sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + // We're not validating the remote host + return true; + } + } +} diff --git a/WindowsDevicePortalWrapper/AppDeployment.cs b/WindowsDevicePortalWrapper/AppDeployment.cs new file mode 100644 index 0000000..3b47505 --- /dev/null +++ b/WindowsDevicePortalWrapper/AppDeployment.cs @@ -0,0 +1,30 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// MOCK implementation of App Deployment methods. + /// + public partial class DevicePortal + { + /// + /// API for getting installation status. + /// + /// The status + public async Task GetInstallStatusAsync() + { + ApplicationInstallStatus status = ApplicationInstallStatus.Completed; + + return await Task.FromResult(status); + } + } +} diff --git a/WindowsDevicePortalWrapper/ApplicationManager.cs b/WindowsDevicePortalWrapper/ApplicationManager.cs new file mode 100644 index 0000000..460e829 --- /dev/null +++ b/WindowsDevicePortalWrapper/ApplicationManager.cs @@ -0,0 +1,160 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for Application Management. + /// + public partial class DevicePortal + { + /// + /// IoT device application list API. + /// + public static readonly string AppsListApi = "api/iot/appx/default"; + + /// + /// IoT device headless application list API. + /// + public static readonly string HeadlessAppsListApi = "api/iot/appx/listHeadlessApps"; + + /// + /// IoT device headless startup application API. + /// + public static readonly string HeadlessStartupAppApi = "api/iot/appx/startupHeadlessApp"; + + /// + /// IoT device package activation API. + /// + public static readonly string ActivatePackageApi = "api/iot/appx/app"; + + /// + /// Gets List of apps. + /// + /// Object containing the list of applications. + public async Task GetAppsListInfoAsync() + { + return await this.GetAsync(AppsListApi); + } + + /// + /// Gets list of headless apps. + /// + /// Object containing the list of headless applications. + public async Task GetHeadlessAppsListInfoAsync() + { + return await this.GetAsync(HeadlessAppsListApi); + } + + /// + /// Sets selected app as the startup app. + /// + /// App Id. + /// Task tracking completion of the REST call. + public async Task UpdateStartupAppAsync(string appId) + { + await this.PostAsync( + AppsListApi, + string.Format("appid={0}", Utilities.Hex64Encode(appId))); + } + + /// + /// Sets the selected app as the headless startup app. + /// + /// App Id. + /// Task tracking completion of the REST call. + public async Task UpdateHeadlessStartupAppAsync(string appId) + { + await this.PostAsync( + HeadlessStartupAppApi, + string.Format("appid={0}", Utilities.Hex64Encode(appId))); + } + + /// + /// Removes the selected app from the headless startup app list. + /// + /// App Id. + /// Task tracking completion of the REST call. + public async Task RemoveHeadlessStartupAppAsync(string appId) + { + await this.DeleteAsync( + HeadlessStartupAppApi, + string.Format("appid={0}", Utilities.Hex64Encode(appId))); + } + + /// + /// Activiates the selected app package. + /// + /// App Id. + /// Task tracking completion of the REST call. + public async Task ActivatePackageAsync(string appId) + { + await this.PostAsync( + ActivatePackageApi, + string.Format("appid={0}", Utilities.Hex64Encode(appId))); + } + #region Data contract + + /// + /// Application list info. + /// + [DataContract] + public class AppsListInfo + { + /// + /// Gets the default application + /// + [DataMember(Name = "DefaultApp")] + public string DefaultApp { get; private set; } + + /// + /// Gets the application packages + /// + [DataMember(Name = "AppPackages")] + public List AppPackages { get; private set; } + } + + /// + /// Application package. + /// + [DataContract] + public class AppPackage + { + /// + /// Gets a value indicating whether the app is the startup app + /// + [DataMember(Name = "IsStartup")] + public bool IsStartup { get; private set; } + + /// + /// Gets the complate package name + /// + [DataMember(Name = "PackageFullName")] + public string PackageFullName { get; private set; } + } + + /// + /// Headless app list information. + /// + [DataContract] + public class HeadlessAppsListInfo + { + /// + /// Gets the list of headless application packages + /// + [DataMember(Name = "AppPackages")] + public List AppPackages { get; private set; } + } + + #endregion // Data contract + } +} diff --git a/WindowsDevicePortalWrapper/CertificateHandling.cs b/WindowsDevicePortalWrapper/CertificateHandling.cs new file mode 100644 index 0000000..831e523 --- /dev/null +++ b/WindowsDevicePortalWrapper/CertificateHandling.cs @@ -0,0 +1,125 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.IO; +using System.Net; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// .net 4.x implementation of device certificate handling methods. + /// + public partial class DevicePortal + { + /// + /// A manually provided certificate for trust validation. + /// + private X509Certificate2 manualCertificate = null; + + /// + /// Gets or sets handler for untrusted certificate handling + /// + public event UnvalidatedCertEventHandler UnvalidatedCert; + + /// + /// Gets the root certificate from the device. + /// + /// The device certificate. + public async Task GetRootDeviceCertificateAsync() + { + X509Certificate2 certificate = null; + + Uri uri = Utilities.BuildEndpoint(this.deviceConnection.Connection, RootCertificateEndpoint); + + using (Stream stream = await this.GetAsync(uri)) + { + using (BinaryReader reader = new BinaryReader(stream)) + { + byte[] certData = reader.ReadBytes((int)stream.Length); + certificate = new X509Certificate2(certData); + } + } + + return certificate; + } + + /// + /// Sets the manual certificate. + /// + /// Manual certificate + private void SetManualCertificate(X509Certificate2 cert) + { + this.manualCertificate = cert; + } + + /// + /// Validate the server certificate + /// + /// The sender object + /// The server's certificate + /// The cert chain + /// Policy Errors + /// whether the cert passes validation + private bool ServerCertificateValidation(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + if (this.manualCertificate != null) + { + chain.ChainPolicy.ExtraStore.Add(this.manualCertificate); + } + + X509Certificate2 certv2 = new X509Certificate2(certificate); + bool isValid = chain.Build(certv2); + + // If chain validation failed but we have a manual cert, we can still + // check the chain to see if the server cert chains up to our manual cert + // (or matches it) in which case this is valid. + if (!isValid && this.manualCertificate != null) + { + foreach (X509ChainElement element in chain.ChainElements) + { + foreach (X509ChainStatus status in element.ChainElementStatus) + { + // Check if this is a failure that should cause the chain to be rejected + if (status.Status != X509ChainStatusFlags.NoError && + status.Status != X509ChainStatusFlags.UntrustedRoot && + status.Status != X509ChainStatusFlags.RevocationStatusUnknown) + { + return false; + } + } + + // This cert chained to our provided cert. Continue walking + // the chain to ensure we don't hit a failure that would + // cause our chain to be rejected. + if (element.Certificate.Issuer == this.manualCertificate.Issuer && + element.Certificate.Thumbprint == this.manualCertificate.Thumbprint) + { + isValid = true; + break; + } + } + } + + // If this still appears invalid, we give the app a chance via a handler + // to override the trust decision. + if (!isValid) + { + bool? overridenIsValid = this.UnvalidatedCert?.Invoke(this, certificate, chain, sslPolicyErrors); + + if (overridenIsValid != null && overridenIsValid == true) + { + isValid = true; + } + } + + return isValid; + } + } +} diff --git a/WindowsDevicePortalWrapper/Core/AppCrashDumpCollection.cs b/WindowsDevicePortalWrapper/Core/AppCrashDumpCollection.cs new file mode 100644 index 0000000..643efed --- /dev/null +++ b/WindowsDevicePortalWrapper/Core/AppCrashDumpCollection.cs @@ -0,0 +1,223 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for app crash dump collection methods. + /// + public partial class DevicePortal + { + /// + /// API to retrieve list of the available crash dumps (for sideloaded applications). + /// + public static readonly string AvailableCrashDumpsApi = "api/debug/dump/usermode/dumps"; + + /// + /// API to download or delete a crash dump file (for a sideloaded application). + /// + public static readonly string CrashDumpFileApi = "api/debug/dump/usermode/crashdump"; + + /// + /// API to control the crash dump settings for a sideloaded application. + /// + public static readonly string CrashDumpSettingsApi = "api/debug/dump/usermode/crashcontrol"; + + /// + /// Get a list of app crash dumps on the device. + /// + /// List of AppCrashDump objects, which represent crashdumps on the device. + public async Task> GetAppCrashDumpListAsync() + { + AppCrashDumpList cdl = await this.GetAsync(AvailableCrashDumpsApi); + return cdl.CrashDumps; + } + + /// + /// Download a sideloaded app's crash dump. + /// + /// The AppCrashDump to download + /// Stream of the crash dump + public async Task GetAppCrashDumpAsync(AppCrashDump crashdump) + { + string queryString = CrashDumpFileApi + string.Format("?packageFullName={0}&fileName={1}", crashdump.PackageFullName, crashdump.Filename); + Uri uri = Utilities.BuildEndpoint( + this.deviceConnection.Connection, + queryString); + + return await this.GetAsync(uri); + } + + /// + /// Delete an app crash dump stored on the device. + /// + /// The crashdump to be deleted + /// Task tracking completion of the request. + public async Task DeleteAppCrashDumpAsync(AppCrashDump crashdump) + { + await this.DeleteAsync( + CrashDumpFileApi, + string.Format("packageFullName={0}&fileName={1}", crashdump.PackageFullName, crashdump.Filename)); + } + + /// + /// Get the crash settings for a sideloaded app. + /// + /// The app to get settings for + /// The crash settings for the app + public async Task GetAppCrashDumpSettingsAsync(AppPackage app) + { + return await this.GetAppCrashDumpSettingsAsync(app.PackageFullName); + } + + /// + /// Get the crash settings for a sideloaded app. + /// + /// The app to get settings for + /// The crash settings for the app + public async Task GetAppCrashDumpSettingsAsync(string packageFullname) + { + return await this.GetAsync( + CrashDumpSettingsApi, + string.Format("packageFullName={0}", packageFullname)); + } + + /// + /// Set the crash settings for a sideloaded app. + /// + /// The app to set crash settings for. + /// Whether to enable or disable crash collection for the app. + /// Task tracking completion of the request. + public async Task SetAppCrashDumpSettingsAsync(AppPackage app, bool enable = true) + { + string pfn = app.PackageFullName; + await this.SetAppCrashDumpSettingsAsync(pfn, enable); + } + + /// + /// Set the crash settings for a sideloaded app. + /// + /// The app to set crash settings for. + /// Whether to enable or disable crash collection for the app. + /// Task tracking completion of the request. + public async Task SetAppCrashDumpSettingsAsync(string packageFullName, bool enable = true) + { + if (enable) + { + await this.PostAsync( + CrashDumpSettingsApi, + string.Format("packageFullName={0}", packageFullName)); + } + else + { + await this.DeleteAsync( + CrashDumpSettingsApi, + string.Format("packageFullName={0}", packageFullName)); + } + } + + #region Data contract + + /// + /// Per-app crash dump settings. + /// + [DataContract] + public class AppCrashDumpSettings + { + /// + /// Gets a value indicating whether crash dumps are enabled for the app + /// + [DataMember(Name = "CrashDumpEnabled")] + public bool CrashDumpEnabled + { + get; + private set; + } + } + + /// + /// Represents a crash dump collected from a sideloaded app. + /// + [DataContract] + public class AppCrashDump + { + /// + /// Gets the timestamp of the crash as a string. + /// + [DataMember(Name = "FileDate")] + public string FileDateAsString + { + get; + private set; + } + + /// + /// Gets the timestamp of the crash. + /// + public DateTime FileDate + { + get + { + return DateTime.Parse(this.FileDateAsString); + } + } + + /// + /// Gets the filename of the crash file. + /// + [DataMember(Name = "FileName")] + public string Filename + { + get; + private set; + } + + /// + /// Gets the size of the crash dump, in bytes + /// + [DataMember(Name = "FileSize")] + public uint FileSizeInBytes + { + get; + private set; + } + + /// + /// Gets the package full name of the app that crashed. + /// + [DataMember(Name = "PackageFullName")] + public string PackageFullName + { + get; + private set; + } + } + + /// + /// A list of crash dumps. Internal usage only. + /// + [DataContract] + private class AppCrashDumpList + { + /// + /// Gets a list of crash dumps on the device. + /// + [DataMember(Name = "CrashDumps")] + public List CrashDumps + { + get; + private set; + } + } + #endregion Data contract + } +} diff --git a/WindowsDevicePortalWrapper/Core/AppDeployment.cs b/WindowsDevicePortalWrapper/Core/AppDeployment.cs new file mode 100644 index 0000000..47d14c4 --- /dev/null +++ b/WindowsDevicePortalWrapper/Core/AppDeployment.cs @@ -0,0 +1,411 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +#if !WINDOWS_UWP +using System.Net; +using System.Net.Http; +#endif // !WINDOWS_UWP +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +#if WINDOWS_UWP +using Windows.Foundation; +using Windows.Security.Credentials; +using Windows.Storage.Streams; +using Windows.Web.Http; +using Windows.Web.Http.Filters; +using Windows.Web.Http.Headers; +#endif + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for App Deployment methods. + /// + public partial class DevicePortal + { + /// + /// API to retrieve list of installed packages. + /// + public static readonly string InstalledPackagesApi = "api/app/packagemanager/packages"; + + /// + /// Install state API. + /// + public static readonly string InstallStateApi = "api/app/packagemanager/state"; + + /// + /// API for package management. + /// + public static readonly string PackageManagerApi = "api/app/packagemanager/package"; + + /// + /// App Install Status handler. + /// + public event ApplicationInstallStatusEventHandler AppInstallStatus; + + /// + /// Gets the collection of applications installed on the device. + /// + /// AppPackages object containing the list of installed application packages. + public async Task GetInstalledAppPackagesAsync() + { + return await this.GetAsync(InstalledPackagesApi); + } + + /// + /// Installs an application + /// + /// Friendly name (ex: Hello World) of the application. If this parameter is not provided, the name of the package is assumed to be the app name. + /// Full name of the application package file. + /// List containing the full names of any required dependency files. + /// Full name of the optional certificate file. + /// How frequently we should check the installation state. + /// Operation timeout. + /// Indicate whether or not the previous app version should be uninstalled prior to installing. + /// InstallApplication sends ApplicationInstallStatus events to indicate the current progress in the installation process. + /// Some applications may opt to not register for the AppInstallStatus event and await on InstallApplication. + /// Task for tracking completion of install initialization. + public async Task InstallApplicationAsync( + string appName, + string packageFileName, + List dependencyFileNames, + string certificateFileName = null, + short stateCheckIntervalMs = 500, + short timeoutInMinutes = 15, + bool uninstallPreviousVersion = true) + { + string installPhaseDescription = string.Empty; + + try + { + FileInfo packageFile = new FileInfo(packageFileName); + + // If appName was not provided, use the package file name + if (string.IsNullOrEmpty(appName)) + { + appName = packageFile.Name; + } + + // Uninstall the application's previous version, if one exists. + if (uninstallPreviousVersion) + { + installPhaseDescription = string.Format("Uninstalling any previous version of {0}", appName); + this.SendAppInstallStatus( + ApplicationInstallStatus.InProgress, + ApplicationInstallPhase.UninstallingPreviousVersion, + installPhaseDescription); + AppPackages installedApps = await this.GetInstalledAppPackagesAsync(); + foreach (PackageInfo package in installedApps.Packages) + { + if (package.Name == appName) + { + await this.UninstallApplicationAsync(package.FullName); + break; + } + } + } + + // Create the API endpoint and generate a unique boundary string. + Uri uri; + string boundaryString; + this.CreateAppInstallEndpointAndBoundaryString( + packageFile.Name, + out uri, + out boundaryString); + + installPhaseDescription = string.Format("Copying: {0}", packageFile.Name); + this.SendAppInstallStatus( + ApplicationInstallStatus.InProgress, + ApplicationInstallPhase.CopyingFile, + installPhaseDescription); + + var content = new HttpMultipartFileContent(); + content.Add(packageFile.FullName); + content.AddRange(dependencyFileNames); + content.Add(certificateFileName); + await this.PostAsync(uri, content); + + // Poll the status until complete. + ApplicationInstallStatus status = ApplicationInstallStatus.InProgress; + do + { + installPhaseDescription = string.Format("Installing {0}", appName); + this.SendAppInstallStatus( + ApplicationInstallStatus.InProgress, + ApplicationInstallPhase.Installing, + installPhaseDescription); + + await Task.Delay(TimeSpan.FromMilliseconds(stateCheckIntervalMs)); + + status = await this.GetInstallStatusAsync().ConfigureAwait(false); + } + while (status == ApplicationInstallStatus.InProgress); + + installPhaseDescription = string.Format("{0} installed successfully", appName); + this.SendAppInstallStatus( + ApplicationInstallStatus.Completed, + ApplicationInstallPhase.Idle, + installPhaseDescription); + } + catch (Exception e) + { + DevicePortalException dpe = e as DevicePortalException; + + if (dpe != null) + { + this.SendAppInstallStatus( + ApplicationInstallStatus.Failed, + ApplicationInstallPhase.Idle, + string.Format("Failed to install {0}: {1}", appName, dpe.Reason)); + } + else + { + this.SendAppInstallStatus( + ApplicationInstallStatus.Failed, + ApplicationInstallPhase.Idle, + string.Format("Failed to install {0}: {1}", appName, installPhaseDescription)); + } + + throw; + } + } + + /// + /// Uninstalls the specified application. + /// + /// The name of the application package to uninstall. + /// Task tracking the uninstall operation. + public async Task UninstallApplicationAsync(string packageName) + { + await this.DeleteAsync( + PackageManagerApi, + //// NOTE: When uninstalling an app package, the package name is not Hex64 encoded. + string.Format("package={0}", packageName)); + } + + /// + /// Builds the application installation Uri and generates a unique boundary string for the multipart form data. + /// + /// The name of the application package. + /// The endpoint for the install request. + /// Unique string used to separate the parts of the multipart form data. + private void CreateAppInstallEndpointAndBoundaryString( + string packageName, + out Uri uri, + out string boundaryString) + { + uri = Utilities.BuildEndpoint( + this.deviceConnection.Connection, + PackageManagerApi, + string.Format("package={0}", packageName)); + + boundaryString = Guid.NewGuid().ToString(); + } + + /// + /// Sends application install status. + /// + /// Status of the installation. + /// Current installation phase (ex: Uninstalling previous version) + /// Optional error message describing the install status. + private void SendAppInstallStatus( + ApplicationInstallStatus status, + ApplicationInstallPhase phase, + string message = "") + { + this.AppInstallStatus?.Invoke( + this, + new ApplicationInstallStatusEventArgs(status, phase, message)); + } + + #region Data contract + /// + /// Object representing a list of Application Packages + /// + [DataContract] + public class AppPackages + { + /// + /// Gets a list of the packages + /// + [DataMember(Name = "InstalledPackages")] + public List Packages { get; private set; } + + /// + /// Presents a user readable representation of a list of AppPackages + /// + /// User readable list of AppPackages. + public override string ToString() + { + string output = "Packages:\n"; + foreach (PackageInfo package in this.Packages) + { + output += package; + } + + return output; + } + } + + /// + /// Object representing the install state + /// + [DataContract] + public class InstallState + { + /// + /// Gets install state code + /// + [DataMember(Name = "Code")] + public int Code { get; private set; } + + /// + /// Gets message text + /// + [DataMember(Name = "CodeText")] + public string CodeText { get; private set; } + + /// + /// Gets reason for state + /// + [DataMember(Name = "Reason")] + public string Reason { get; private set; } + + /// + /// Gets a value indicating whether this was successful + /// + [DataMember(Name = "Success")] + public bool WasSuccessful { get; private set; } + } + + /// + /// object representing the package information + /// + [DataContract] + public class PackageInfo + { + /// + /// Gets package name + /// + [DataMember(Name = "Name")] + public string Name { get; private set; } + + /// + /// Gets package family name + /// + [DataMember(Name = "PackageFamilyName")] + public string FamilyName { get; private set; } + + /// + /// Gets package full name + /// + [DataMember(Name = "PackageFullName")] + public string FullName { get; private set; } + + /// + /// Gets package relative Id + /// + [DataMember(Name = "PackageRelativeId")] + public string AppId { get; private set; } + + /// + /// Gets package publisher + /// + [DataMember(Name = "Publisher")] + public string Publisher { get; private set; } + + /// + /// Gets package version + /// + [DataMember(Name = "Version")] + public PackageVersion Version { get; private set; } + + /// + /// Gets package origin, a measure of how the app was installed. + /// PackageOrigin_Unknown            = 0, + /// PackageOrigin_Unsigned           = 1, + /// PackageOrigin_Inbox              = 2, + /// PackageOrigin_Store              = 3, + /// PackageOrigin_DeveloperUnsigned  = 4, + /// PackageOrigin_DeveloperSigned    = 5, + /// PackageOrigin_LineOfBusiness     = 6 + /// + [DataMember(Name = "PackageOrigin")] + public int PackageOrigin { get; private set; } + + /// + /// Helper method to determine if the app was sideloaded and therefore can be used with e.g. GetFolderContentsAsync + /// + /// True if the package is sideloaded. + public bool IsSideloaded() + { + return this.PackageOrigin == 4 || this.PackageOrigin == 5; + } + + /// + /// Get a string representation of the package + /// + /// String representation + public override string ToString() + { + return string.Format("\t{0}\n\t\t{1}\n", this.FullName, this.AppId); + } + } + + /// + /// Object representing a package version + /// + [DataContract] + public class PackageVersion + { + /// + /// Gets version build + /// + [DataMember(Name = "Build")] + public int Build { get; private set; } + + /// + /// Gets package Major number + /// + [DataMember(Name = "Major")] + public int Major { get; private set; } + + /// + /// Gets package minor number + /// + [DataMember(Name = "Minor")] + public int Minor { get; private set; } + + /// + /// Gets package revision + /// + [DataMember(Name = "Revision")] + public int Revision { get; private set; } + + /// + /// Gets package version + /// + public Version Version + { + get { return new Version(this.Major, this.Minor, this.Build, this.Revision); } + } + + /// + /// Get a string representation of a version + /// + /// String representation + public override string ToString() + { + return Version.ToString(); + } + } + #endregion // Data contract + } +} diff --git a/WindowsDevicePortalWrapper/Core/AppFileExplorer.cs b/WindowsDevicePortalWrapper/Core/AppFileExplorer.cs new file mode 100644 index 0000000..24caa2f --- /dev/null +++ b/WindowsDevicePortalWrapper/Core/AppFileExplorer.cs @@ -0,0 +1,351 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for App File explorer methods + /// + public partial class DevicePortal + { + /// + /// API to upload, download or delete a file in a folder. + /// + public static readonly string GetFileApi = "api/filesystem/apps/file"; + + /// + /// API to rename a file in a folder. + /// + public static readonly string RenameFileApi = "api/filesystem/apps/rename"; + + /// + /// API to retrieve the list of files in a folder. + /// + public static readonly string GetFilesApi = "api/filesystem/apps/files"; + + /// + /// API to retrieve the list of accessible top-level folders. + /// + public static readonly string KnownFoldersApi = "api/filesystem/apps/knownfolders"; + + /// + /// Gets a list of Known Folders on the device. + /// + /// List of known folders available on this device. + public async Task GetKnownFoldersAsync() + { + return await this.GetAsync(KnownFoldersApi); + } + + /// + /// Gets a list of files in a Known Folder (e.g. LocalAppData). + /// + /// The known folder id for the root of the path. + /// An optional subpath to the folder. + /// The package full name if using LocalAppData. + /// Contents of the requested folder. + public async Task GetFolderContentsAsync( + string knownFolderId, + string subPath = null, + string packageFullName = null) + { + Dictionary payload = this.BuildCommonFilePayload(knownFolderId, subPath, packageFullName); + + return await this.GetAsync(GetFilesApi, Utilities.BuildQueryString(payload)); + } + + /// + /// Gets a file from LocalAppData or another Known Folder on the device. + /// + /// The known folder id for the root of the path. + /// The name of the file we are downloading. + /// An optional subpath to the folder. + /// The package full name if using LocalAppData. + /// Stream to the downloaded file. + public async Task GetFileAsync( + string knownFolderId, + string filename, + string subPath = null, + string packageFullName = null) + { + Dictionary payload = this.BuildCommonFilePayload(knownFolderId, subPath, packageFullName); + + filename = WebUtility.UrlEncode(filename); + payload.Add("filename", filename); + + Uri uri = Utilities.BuildEndpoint( + this.deviceConnection.Connection, + GetFileApi, + Utilities.BuildQueryString(payload)); + + return await this.GetAsync(uri); + } + + /// + /// Uploads a file to a Known Folder (e.g. LocalAppData) + /// + /// The known folder id for the root of the path. + /// The path to the file we are uploading. + /// An optional subpath to the folder. + /// The package full name if using LocalAppData. + /// Task tracking completion of the upload request. + public async Task UploadFileAsync( + string knownFolderId, + string filepath, + string subPath = null, + string packageFullName = null) + { + Dictionary payload = this.BuildCommonFilePayload(knownFolderId, subPath, packageFullName); + + List files = new List(); + files.Add(filepath); + + await this.PostAsync(GetFileApi, files, Utilities.BuildQueryString(payload)); + } + + /// + /// Deletes a file from a Known Folder. + /// + /// The known folder id for the root of the path. + /// The name of the file we are deleting. + /// An optional subpath to the folder. + /// The package full name if using LocalAppData. + /// Task tracking completion of the delete request. + public async Task DeleteFileAsync( + string knownFolderId, + string filename, + string subPath = null, + string packageFullName = null) + { + Dictionary payload = this.BuildCommonFilePayload(knownFolderId, subPath, packageFullName); + filename = WebUtility.UrlEncode(filename); + payload.Add("filename", filename); + + await this.DeleteAsync(GetFileApi, Utilities.BuildQueryString(payload)); + } + + /// + /// Renames a file in a Known Folder. + /// + /// The known folder id for the root of the path. + /// The name of the file we are renaming. + /// The new name for this file. + /// An optional subpath to the folder. + /// The package full name if using LocalAppData. + /// Task tracking completion of the rename request. + public async Task RenameFileAsync( + string knownFolderId, + string filename, + string newFilename, + string subPath = null, + string packageFullName = null) + { + Dictionary payload = this.BuildCommonFilePayload(knownFolderId, subPath, packageFullName); + + payload.Add("filename", filename); + payload.Add("newfilename", newFilename); + + await this.PostAsync(RenameFileApi, Utilities.BuildQueryString(payload)); + } + + /// + /// Do some common parsing and validation of file explorer parameters. + /// + /// The known folder id. + /// The optional subpath for the folder. + /// The packagefullname if using LocalAppData. + /// Dictionary of param name to value. + private Dictionary BuildCommonFilePayload(string knownFolderId, string subPath, string packageFullName) + { + Dictionary payload = new Dictionary(); + + payload.Add("knownfolderid", knownFolderId); + + if (!string.IsNullOrEmpty(subPath)) + { + if (!subPath.StartsWith("/")) + { + subPath = subPath.Insert(0, "/"); + } + + payload.Add("path", subPath); + } + + if (!string.IsNullOrEmpty(packageFullName)) + { + payload.Add("packagefullname", packageFullName); + } + else if (string.Equals(knownFolderId, "LocalAppData", StringComparison.OrdinalIgnoreCase)) + { + throw new Exception("LocalAppData requires a packageFullName be provided."); + } + + return payload; + } + + #region Data contract + + /// + /// Known Folders object. + /// + [DataContract] + public class KnownFolders + { + /// + /// Gets the list of known folders. + /// + [DataMember(Name = "KnownFolders")] + public List Folders { get; private set; } + + /// + /// Overridden ToString method providing a user readable + /// list of known folders. + /// + /// String representation of the object. + public override string ToString() + { + if (this.Folders == null) + { + return string.Empty; + } + + string contents = string.Empty; + foreach (string folder in this.Folders) + { + contents += folder + '\n'; + } + + return contents; + } + } + + /// + /// Folder contents object. + /// + [DataContract] + public class FolderContents + { + /// + /// Gets the list of folders and files in this folder. + /// + [DataMember(Name = "Items")] + public List Contents { get; private set; } + + /// + /// Overridden ToString method providing a user readable + /// display of a folder's contents. Tries to match the formatting + /// of regular DIR commands. + /// + /// String representation of the object. + public override string ToString() + { + if (this.Contents == null) + { + return string.Empty; + } + + string contents = string.Empty; + foreach (FileOrFolderInformation fileOrFolder in this.Contents) + { + contents += fileOrFolder.ToString(); + } + + return contents; + } + } + + /// + /// Details about a folder or file. + /// + [DataContract] + public class FileOrFolderInformation + { + /// + /// Gets the current directory. + /// + [DataMember(Name = "CurrentDir")] + public string CurrentDir { get; private set; } + + /// + /// Gets the current directory. + /// + [DataMember(Name = "DateCreated")] + public long DateCreated { get; private set; } + + /// + /// Gets the Id. + /// + [DataMember(Name = "Id")] + public string Id { get; private set; } + + /// + /// Gets the Name. + /// + [DataMember(Name = "Name")] + public string Name { get; private set; } + + /// + /// Gets the SubPath (equivalent to CurrentDir for files). + /// + [DataMember(Name = "SubPath")] + public string SubPath { get; private set; } + + /// + /// Gets the Type. + /// + [DataMember(Name = "Type")] + public int Type { get; private set; } + + /// + /// Gets the size of the file (0 for folders). + /// + [DataMember(Name = "FileSize")] + public long SizeInBytes { get; private set; } + + /// + /// Gets a value indicating whether the current item is a folder by checking for FILE_ATTRIBUTE_DIRECTORY + /// See https://msdn.microsoft.com/en-us/library/windows/desktop/gg258117(v=vs.85).aspx + /// + public bool IsFolder + { + get + { + return (this.Type & 0x10) == 0x10; + } + } + + /// + /// Overridden ToString method providing a user readable + /// display of a file or folder. Tries to match the formatting + /// of regular DIR commands. + /// + /// String representation of the object. + public override string ToString() + { + DateTime timestamp = DateTime.FromFileTime(this.DateCreated); + + // Check if this is a folder. + if (this.IsFolder) + { + return string.Format("{0,-24:MM/dd/yyyy HH:mm tt}{1,-14} {2}\n", timestamp, "", this.Name); + } + else + { + return string.Format("{0,-24:MM/dd/yyyy HH:mm tt}{1,14:n0} {2}\n", timestamp, this.SizeInBytes, this.Name); + } + } + } + + #endregion + } +} diff --git a/WindowsDevicePortalWrapper/Core/DeviceManager.cs b/WindowsDevicePortalWrapper/Core/DeviceManager.cs new file mode 100644 index 0000000..d420b83 --- /dev/null +++ b/WindowsDevicePortalWrapper/Core/DeviceManager.cs @@ -0,0 +1,103 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for device management methods. + /// + public partial class DevicePortal + { + /// + /// API to retrieve list of installed devices. + /// + public static readonly string InstalledDevicesApi = "api/devicemanager/devices"; + + /// + /// Get a listing of installed devices + /// + /// List of installed devices + public async Task> GetDeviceListAsync() + { + DeviceList deviceList = await this.GetAsync(InstalledDevicesApi); + return deviceList.Devices; + } + + #region Data contract + /// + /// Object representing a device entry + /// + [DataContract] + public class DeviceList + { + /// + /// Gets the Device Class + /// + [DataMember(Name = "DeviceList")] + public List Devices { get; private set; } + } + + /// + /// Object representing a device entry + /// + [DataContract] + public class Device + { + /// + /// Gets the Device Class + /// + [DataMember(Name = "Class")] + public string Class { get; private set; } + + /// + /// Gets the Device Description + /// + [DataMember(Name = "Description")] + public string Description { get; private set; } + + /// + /// Gets the friendly (human-readable) name for the device. Usually more descriptive than Description. Does not apply to all Devices. + /// + [DataMember(Name = "FriendlyName")] + public string FriendlyName { get; private set; } + + /// + /// Gets the Device ID + /// + [DataMember(Name = "ID")] + public string ID { get; private set; } + + /// + /// Gets the Device Manufacturer + /// + [DataMember(Name = "Manufacturer")] + public string Manufacturer { get; private set; } + + /// + /// Gets the Device ParentID, used for pairing + /// + [DataMember(Name = "ParentID")] + public string ParentID { get; private set; } + + /// + /// Gets the Device Problem Code + /// + [DataMember(Name = "ProblemCode")] + public int ProblemCode { get; private set; } + + /// + /// Gets the Device Status Code + /// + [DataMember(Name = "StatusCode")] + public int StatusCode { get; private set; } + } + #endregion + } +} diff --git a/WindowsDevicePortalWrapper/Core/Dns-Sd.cs b/WindowsDevicePortalWrapper/Core/Dns-Sd.cs new file mode 100644 index 0000000..82d4bb1 --- /dev/null +++ b/WindowsDevicePortalWrapper/Core/Dns-Sd.cs @@ -0,0 +1,92 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for DNS methods + /// + public partial class DevicePortal + { + /// + /// API to add or delete a tag to the DNS-SD advertisement. + /// + public static readonly string TagApi = "api/dns-sd/tag"; + + /// + /// API to retrieve or delete the currently applied tags for the device. + /// + public static readonly string TagsApi = "api/dns-sd/tags"; + + /// + /// Gets a list of DNS-SD tags being broadcast by this device. + /// + /// Array of strings, each one an individual tag. + public async Task> GetServiceTagsAsync() + { + ServiceTags tags = await this.GetAsync(TagsApi); + return tags.Tags; + } + + /// + /// Adds a tag to this device's DNS-SD broadcast. + /// + /// The tag to assign to the device. + /// Task tracking adding the tag. + public async Task AddServiceTagAsync(string tagValue) + { + await this.PostAsync( + TagApi, + string.Format("tagValue={0}", tagValue)); + } + + /// + /// Delete all tags from the device's DNS-SD broadcast. + /// + /// Task tracking deletion of all tags. + public async Task DeleteAllTagsAsync() + { + await this.DeleteAsync(TagsApi); + } + + /// + /// Delete a specific tag from the device's DNS-SD broadcast. + /// + /// The tag to delete from the device broadcast. + /// Task tracking deletion of the tag. + public async Task DeleteTagAsync(string tagValue) + { + await this.DeleteAsync( + TagApi, + string.Format("tagValue={0}", tagValue)); + } + + #region Data contract + + /// + /// Service tags object + /// + [DataContract] + public class ServiceTags + { + /// + /// Gets the DNS-SD service tags + /// + [DataMember(Name = "tags")] + public List Tags + { + get; private set; + } + + #endregion Data contract + } + } +} diff --git a/WindowsDevicePortalWrapper/Core/DumpCollection.cs b/WindowsDevicePortalWrapper/Core/DumpCollection.cs new file mode 100644 index 0000000..641341e --- /dev/null +++ b/WindowsDevicePortalWrapper/Core/DumpCollection.cs @@ -0,0 +1,242 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for crash dump collection methods. + /// + public partial class DevicePortal + { + /// + /// API to retrieve list of the available bugcheck minidumps. + /// + public static readonly string AvailableBugChecksApi = "api/debug/dump/kernel/dumplist"; + + /// + /// API to download a bugcheck minidump file. + /// + public static readonly string BugcheckFileApi = "api/debug/dump/kernel/dump"; + + /// + /// API to control bugcheck minidump settings. + /// + public static readonly string BugcheckSettingsApi = "api/debug/dump/kernel/crashcontrol"; + + /// + /// API to retrieve a live kernel dump. + /// + public static readonly string LiveKernelDumpApi = "api/debug/dump/livekernel"; + + /// + /// API to retrieve a live dump from a running user mode process. + /// + public static readonly string LiveProcessDumpApi = "api/debug/dump/usermode/live"; + + /// + /// Get a list of dumpfiles on the system. + /// + /// List of Dumpfiles. Use GetDumpFile to download the file. + public async Task> GetDumpfileListAsync() + { + DumpFileList dfl = await this.GetAsync(AvailableBugChecksApi); + return dfl.DumpFiles; + } + + /// + /// Download a dumpfile from the system. + /// + /// Dumpfile object to be downloaded + /// Stream of the dump file + public async Task GetDumpFileAsync(Dumpfile crashdump) + { + string queryString = BugcheckFileApi + string.Format("?filename={0}", crashdump.Filename); + Uri uri = Utilities.BuildEndpoint( + this.deviceConnection.Connection, + queryString); + + return await this.GetAsync(uri); + } + + /// + /// Takes a live kernel dump of the device. This does not reboot the device. + /// + /// Stream of the kernel dump + public async Task GetLiveKernelDumpAsync() + { + Uri uri = Utilities.BuildEndpoint( + this.deviceConnection.Connection, + LiveKernelDumpApi); + + return await this.GetAsync(uri); + } + + /// + /// Take a live dump of a process on the system. + /// + /// PID of the process to get a live dump of. + /// Stream of the process live dump + public async Task GetLiveProcessDumpAsync(int pid) + { + string queryString = LiveProcessDumpApi + string.Format("?pid={0}", pid); + Uri uri = Utilities.BuildEndpoint( + this.deviceConnection.Connection, + queryString); + + return await this.GetAsync(uri); + } + + /// + /// Get information on how and when dump files are saved on the device. + /// + /// Dumpfile settings object. This can be edited and returned to the device to alter the settings. + public async Task GetDumpFileSettingsAsync() + { + return await this.GetAsync(BugcheckSettingsApi); + } + + /// + /// Set how and when dump files are saved on the device. + /// + /// Altered DumpFileSettings object to set on the device + /// Task tracking completion of the request + public async Task SetDumpFileSettingsAsync(DumpFileSettings dfs) + { + string queryParams = string.Format( + "autoreboot={0}&overwrite={1}&dumptype={2}&maxdumpcount={3}", + dfs.AutoReboot ? "1" : "0", + dfs.Overwrite ? "1" : "0", + (int)dfs.DumpType, + dfs.MaxDumpCount); + + await this.PostAsync(BugcheckSettingsApi, queryParams); + } + + #region Data Contract + + /// + /// DumpFileSettings object. Used to get and set how and when a dump is saved on the device. + /// + [DataContract] + public class DumpFileSettings + { + /// + /// The 3 types of dumps that can be saved on the device (or not saved at all). + /// + public enum DumpTypes + { + /// + /// Don't collect device crash dumps + /// + Disabled = 0, + + /// + /// Collect all in use memory + /// + CompleteMemoryDump = 1, + + /// + /// Don't include usermode memory in the dump + /// + KernelDump = 2, + + /// + /// Limited kernel dump + /// + Minidump = 3 + } + + /// + /// Gets or sets a value indicating whether the device should restart after a crash dump is taken. + /// + [DataMember(Name = "autoreboot")] + public bool AutoReboot { get; set; } + + /// + /// Gets or sets the type of dump to be saved when a bugcheck occurs. + /// + [DataMember(Name = "dumptype")] + public DumpTypes DumpType { get; set; } + + /// + /// Gets or sets the max number of dumps to be saved on the device. + /// + [DataMember(Name = "maxdumpcount")] + public int MaxDumpCount { get; set; } + + /// + /// Gets or sets a value indicating whether new dumps should overwrite older dumps. + /// + [DataMember(Name = "overwrite")] + public bool Overwrite { get; set; } + } + + /// + /// Gets a list of kernel dumps on the device. + /// + [DataContract] + public class DumpFileList + { + /// + /// Gets a list of kernel dumps on the device. + /// + [DataMember(Name = "DumpFiles")] + public List DumpFiles { get; private set; } + } + + /// + /// Represents a dumpfile stored on the device. + /// + [DataContract] + public class Dumpfile + { + /// + /// Gets the timestamp of the crash as a string. + /// + [DataMember(Name = "FileDate")] + public string FileDateAsString + { + get; private set; + } + + /// + /// Gets the timestamp of the crash. + /// + public DateTime FileDate + { + get + { + return DateTime.Parse(this.FileDateAsString); + } + } + + /// + /// Gets the filename of the crash file. + /// + [DataMember(Name = "FileName")] + public string Filename + { + get; private set; + } + + /// + /// Gets the size of the crash dump, in bytes + /// + [DataMember(Name = "FileSize")] + public uint FileSizeInBytes + { + get; private set; + } + } + #endregion Data Contract + } +} diff --git a/WindowsDevicePortalWrapper/Core/Etw.cs b/WindowsDevicePortalWrapper/Core/Etw.cs new file mode 100644 index 0000000..eef4e61 --- /dev/null +++ b/WindowsDevicePortalWrapper/Core/Etw.cs @@ -0,0 +1,317 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for ETW methods + /// + public partial class DevicePortal + { + /// + /// API to create a realtime ETW session. + /// + public static readonly string RealtimeEtwSessionApi = "api/etw/session/realtime"; + + /// + /// API to get the list of registered custom ETW providers. + /// + public static readonly string CustomEtwProvidersApi = "api/etw/customproviders"; + + /// + /// API to get the list of registered ETW providers. + /// + public static readonly string EtwProvidersApi = "api/etw/providers"; + + /// + /// Web socket to get ETW events. + /// + private WebSocket realtimeEventsWebSocket; + + /// + /// Determines if the event listener has been registered + /// + private bool isListeningForRealtimeEvents = false; + + /// + /// The ETW event message received handler + /// + public event WebSocketMessageReceivedEventHandler RealtimeEventsMessageReceived; + + /// + /// Returns the current set of registered custom ETW providers. + /// + /// EtwProviders object containing a list of providers, friendly name and GUID + public async Task GetCustomEtwProvidersAsync() + { + return await this.GetAsync(CustomEtwProvidersApi); + } + + /// + /// Returns the current set of registered ETW providers. + /// + /// EtwProviders object containing a list of providers, friendly name and GUID + public async Task GetEtwProvidersAsync() + { + return await this.GetAsync(EtwProvidersApi); + } + + /// + /// Toggles the listening state of a specific provider on the realtime events WebSocket. + /// + /// The provider to update the listening state of. + /// Determines whether the listening state should be enabled or disabled. + /// /// Verbosity level - 1 for least, 5 for most verbose. + /// Task for toggling the listening state of the specified provider. + public async Task ToggleEtwProviderAsync(EtwProviderInfo etwProvider, bool isEnabled = true, int level = 5) + { + await this.ToggleEtwProviderAsync(etwProvider.GUID, isEnabled, level); + } + + /// + /// Toggles the listening state of a specific provider on the realtime events WebSocket. + /// + /// The GUID of the provider to update the listening state of. + /// Determines whether the listening state should be enabled or disabled. + /// Verbosity level - 1 for least, 5 for most verbose. + /// Task for toggling the listening state of the specified provider. + public async Task ToggleEtwProviderAsync(Guid etwProvider, bool isEnabled = true, int level = 5) + { + string action = isEnabled ? "enable" : "disable"; + string message = $"provider {etwProvider} {action} {level}"; + + await this.InitializeRealtimeEventsWebSocketAsync(); + await this.realtimeEventsWebSocket.SendMessageAsync(message); + } + + /// + /// Starts listening for ETW events with it being returned via the RealtimeEventsMessageReceived event handler. + /// + /// Task for connecting to the WebSocket but not for listening to it. + public async Task StartListeningForEtwEventsAsync() + { + await this.InitializeRealtimeEventsWebSocketAsync(); + + if (!this.isListeningForRealtimeEvents) + { + this.isListeningForRealtimeEvents = true; + this.realtimeEventsWebSocket.WebSocketMessageReceived += this.EtwEventsReceivedHandler; + } + + await this.realtimeEventsWebSocket.ReceiveMessagesAsync(); + } + + /// + /// Stop listening for ETW events. + /// + /// Task for stop listening for ETW events and disconnecting from the WebSocket. + public async Task StopListeningForEtwEventsAsync() + { + if (this.isListeningForRealtimeEvents) + { + this.isListeningForRealtimeEvents = false; + this.realtimeEventsWebSocket.WebSocketMessageReceived -= this.EtwEventsReceivedHandler; + } + + await this.realtimeEventsWebSocket.CloseAsync(); + } + + /// + /// Creates a new if it hasn't already been initialized. + /// + /// Task for connecting the ETW realtime event WebSocket. + private async Task InitializeRealtimeEventsWebSocketAsync() + { + if (this.realtimeEventsWebSocket == null) + { +#if WINDOWS_UWP + this.realtimeEventsWebSocket = new WebSocket(this.deviceConnection); +#else + this.realtimeEventsWebSocket = new WebSocket(this.deviceConnection, this.ServerCertificateValidation); +#endif + } + + await this.realtimeEventsWebSocket.ConnectAsync(RealtimeEtwSessionApi); + } + + /// + /// Handler for the ETW received event that passes the event to the RealtimeEventsMessageReceived handler. + /// + /// The object sending the event. + /// The event data. + private void EtwEventsReceivedHandler( + WebSocket sender, + WebSocketMessageReceivedEventArgs args) + { + if (args.Message != null) + { + this.RealtimeEventsMessageReceived?.Invoke( + this, + args); + } + } + + #region Data contract + + /// + /// ETW Events. + /// + [DataContract] + public class EtwEvents + { + /// + /// Saves the downconverted list of events + /// + private List stashedList; + + /// + /// Gets the list of ETW Events that occured in the last second. + /// + public List Events + { + get + { + if (this.stashedList != null) + { + return this.stashedList; + } + + List events = new List(); + foreach (Dictionary dic in this.RawEvents) + { + events.Add(new EtwEventInfo(dic)); + } + + this.stashedList = events; + return this.stashedList; + } + } + + /// + /// Gets the event frequency. + /// This is always 10 million (10000000) in RS2 devices. + /// + [DataMember(Name = "Frequency")] + public long Frequency { get; private set; } + + /// + /// Gets or sets the raw list of events. Not for straight usage, as it's entirely unformatted. + /// + [DataMember(Name = "Events")] + private List> RawEvents { get; set; } + } + + /// + /// ETW Events Info. Allows strongly typed access to guaranteed fields + /// like ID or Timestamp, and raw (as string) access to all other + /// payload data, like Latency or PID. + /// + public class EtwEventInfo : Dictionary + { + /// + /// Initializes a new instance of the class. Used by the DataContract at access time. + /// + /// Base dictionary used to populate the object. + internal EtwEventInfo(IDictionary dictionary) : base(dictionary) + { + } + + /// + /// Gets the event identifer. + /// + public ushort ID + { + get + { + return ushort.Parse(this["ID"]); + } + } + + /// + /// Gets the event keyword. + /// + public ulong Keyword + { + get + { + return ulong.Parse(this["Keyword"]); + } + } + + /// + /// Gets the event level. + /// + public uint Level + { + get + { + return uint.Parse(this["Level"]); + } + } + + /// + /// Gets the event provider name. + /// + public string Provider + { + get + { + return this["ProviderName"]; + } + } + + /// + /// Gets the event timestamp. + /// + public ulong Timestamp + { + get + { + return ulong.Parse(this["Timestamp"]); + } + } + } + + /// + /// ETW Providers. + /// + [DataContract] + public class EtwProviders + { + /// + /// Gets list of ETW providers + /// + [DataMember(Name = "Providers")] + public List Providers { get; private set; } + } + + /// + /// ETW Provider Info. Contains the Name and GUID. + /// + [DataContract] + public class EtwProviderInfo + { + /// + /// Gets provider guid. + /// + [DataMember(Name = "GUID")] + public Guid GUID { get; private set; } + + /// + /// Gets provider name. + /// + [DataMember(Name = "Name")] + public string Name { get; private set; } + } + +#endregion // Data contract + } +} diff --git a/WindowsDevicePortalWrapper/Core/Networking.cs b/WindowsDevicePortalWrapper/Core/Networking.cs new file mode 100644 index 0000000..9b34e54 --- /dev/null +++ b/WindowsDevicePortalWrapper/Core/Networking.cs @@ -0,0 +1,166 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for Networking methods. + /// + public partial class DevicePortal + { + /// + /// API for getting IP config data. + /// + public static readonly string IpConfigApi = "api/networking/ipconfig"; + + /// + /// Gets the IP configuration data of the device. + /// + /// object containing details of the device's network configuration. + public async Task GetIpConfigAsync() + { + return await this.GetAsync(IpConfigApi); + } + + #region Data contract + + /// + /// DHCP object. + /// + [DataContract] + public class Dhcp + { + /// + /// Gets the time at which the lease will expire, in ticks. + /// + [DataMember(Name = "LeaseExpires")] + public long LeaseExpiresRaw { get; private set; } + + /// + /// Gets the time at which the lease was obtained, in ticks. + /// + [DataMember(Name = "LeaseObtained")] + public long LeaseObtainedRaw { get; private set; } + + /// + /// Gets the name. + /// + [DataMember(Name = "Address")] + public IpAddressInfo Address { get; private set; } + + /// + /// Gets the lease expiration time. + /// + public DateTimeOffset LeaseExpires + { + get { return new DateTimeOffset(new DateTime(this.LeaseExpiresRaw)); } + } + + /// + /// Gets the lease obtained time. + /// + public DateTimeOffset LeaseObtained + { + get { return new DateTimeOffset(new DateTime(this.LeaseObtainedRaw)); } + } + } + + /// + /// IP Address info + /// + [DataContract] + public class IpAddressInfo + { + /// + /// Gets the address + /// + [DataMember(Name = "IpAddress")] + public string Address { get; private set; } + + /// + /// Gets the subnet mask + /// + [DataMember(Name = "Mask")] + public string SubnetMask { get; private set; } + } + + /// + /// IP Configuration object + /// + [DataContract] + public class IpConfiguration + { + /// + /// Gets the list of networking adapters + /// + [DataMember(Name = "Adapters")] + public List Adapters { get; private set; } + } + + /// + /// Networking adapter info + /// + [DataContract] + public class NetworkAdapterInfo + { + /// + /// Gets the description + /// + [DataMember(Name = "Description")] + public string Description { get; private set; } + + /// + /// Gets the hardware address + /// + [DataMember(Name = "HardwareAddress")] + public string MacAddress { get; private set; } + + /// + /// Gets the index + /// + [DataMember(Name = "Index")] + public int Index { get; private set; } + + /// + /// Gets the name + /// + [DataMember(Name = "Name")] + public Guid Id { get; private set; } + + /// + /// Gets the type + /// + [DataMember(Name = "Type")] + public string AdapterType { get; private set; } + + /// + /// Gets DHCP info + /// + [DataMember(Name = "DHCP")] + public Dhcp Dhcp { get; private set; } + + // TODO - WINS + + /// + /// Gets Gateway info + /// + [DataMember(Name = "Gateways")] + public List Gateways { get; private set; } + + /// + /// Gets the list of IP addresses + /// + [DataMember(Name = "IpAddresses")] + public List IpAddresses { get; private set; } + } + #endregion // Data contract + } +} diff --git a/WindowsDevicePortalWrapper/Core/OsInformation.cs b/WindowsDevicePortalWrapper/Core/OsInformation.cs new file mode 100644 index 0000000..9ef6ccb --- /dev/null +++ b/WindowsDevicePortalWrapper/Core/OsInformation.cs @@ -0,0 +1,272 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for OS Information. + /// + public partial class DevicePortal + { + /// + /// API for getting the device family. + /// + public static readonly string DeviceFamilyApi = "api/os/devicefamily"; + + /// + /// API for getting the machine name. + /// + public static readonly string MachineNameApi = "api/os/machinename"; + + /// + /// API for getting the OS information. + /// + public static readonly string OsInfoApi = "api/os/info"; + + /// + /// Device portal platforms + /// + public enum DevicePortalPlatforms + { + /// + /// Unknown platform + /// + Unknown = -1, + + /// + /// Windows platform + /// + Windows = 0, + + /// + /// Mobile platform + /// + Mobile, + + /// + /// HoloLens platform + /// + HoloLens, + + /// + /// Xbox One platform + /// + XboxOne, + + /// + /// Windows IoT on Dragonboard 410c + /// + IoTDragonboard410c, + + /// + /// Windows IoT on Minnowboard Max + /// + IoTMinnowboardMax, + + /// + /// Windows IoT on Raspberry Pi 2 + /// + IoTRaspberryPi2, + + /// + /// Windows IoT on Raspberry Pi 3 + /// + IoTRaspberryPi3, + + /// + /// A virtual machine. This may or may not be an emulator. + /// + VirtualMachine + } + + /// + /// Gets the family name (ex: Windows.Holographic) of the device. + /// + /// String containing the device's family. + public async Task GetDeviceFamilyAsync() + { + DeviceOsFamily deviceFamily = await this.GetAsync(DeviceFamilyApi).ConfigureAwait(false); + return deviceFamily.Family; + } + + /// + /// Gets the name of the device. + /// + /// String containing the device's name. + public async Task GetDeviceNameAsync() + { + DeviceName deviceName = await this.GetAsync(MachineNameApi); + return deviceName.Name; + } + + /// + /// Gets information about the device's operating system. + /// + /// OperatingSystemInformation object containing details of the installed operating system. + public Task GetOperatingSystemInformationAsync() + { + return this.GetAsync(OsInfoApi); + } + + /// + /// Sets the device's name + /// + /// The name to assign to the device. + /// The new name does not take effect until the device has been restarted. + /// Task tracking setting the device name completion. + public Task SetDeviceNameAsync(string name) + { + return this.PostAsync( + MachineNameApi, + string.Format("name={0}", Utilities.Hex64Encode(name))); + } + + #region Data contract + + /// + /// Device name object. + /// + [DataContract] + public class DeviceName + { + /// + /// Gets the name. + /// + [DataMember(Name = "ComputerName")] + public string Name { get; private set; } + } + + /// + /// Device family object. + /// + [DataContract] + public class DeviceOsFamily + { + /// + /// Gets the device family name. + /// + [DataMember(Name = "DeviceType")] + public string Family { get; private set; } + } + + /// + /// Operating system information. + /// + [DataContract] + public class OperatingSystemInformation + { + /// + /// Gets the OS name. + /// + [DataMember(Name = "ComputerName")] + public string Name { get; private set; } + + /// + /// Gets the language + /// + [DataMember(Name = "Language")] + public string Language { get; private set; } + + /// + /// Gets the edition + /// + [DataMember(Name = "OsEdition")] + public string OsEdition { get; private set; } + + /// + /// Gets the edition Id + /// + [DataMember(Name = "OsEditionId")] + public uint OsEditionId { get; private set; } + + /// + /// Gets the OS version + /// + [DataMember(Name = "OsVersion")] + public string OsVersionString { get; private set; } + + /// + /// Gets the raw platform type + /// + [DataMember(Name = "Platform")] + public string PlatformName { get; private set; } + + /// + /// Gets the platform + /// + public DevicePortalPlatforms Platform + { + get + { + DevicePortalPlatforms platform = DevicePortalPlatforms.Unknown; + + try + { + // MinnowBoard Max model no. can change based on firmware + if (this.PlatformName.Contains("Minnowboard Max")) + { + return DevicePortalPlatforms.IoTMinnowboardMax; + } + + switch (this.PlatformName) + { + case "Xbox One": + platform = DevicePortalPlatforms.XboxOne; + break; + + case "SBC": + platform = DevicePortalPlatforms.IoTDragonboard410c; + break; + + case "Raspberry Pi 2": + platform = DevicePortalPlatforms.IoTRaspberryPi2; + break; + + case "Raspberry Pi 3": + platform = DevicePortalPlatforms.IoTRaspberryPi3; + break; + + case "Virtual Machine": + platform = DevicePortalPlatforms.VirtualMachine; + break; + + default: + platform = (DevicePortalPlatforms)Enum.Parse(typeof(DevicePortalPlatforms), this.PlatformName); + break; + } + } + catch + { + switch (this.OsEdition) + { + case "Enterprise": + case "Home": + case "Professional": + platform = DevicePortalPlatforms.Windows; + break; + + case "Mobile": + platform = DevicePortalPlatforms.Mobile; + break; + + default: + platform = DevicePortalPlatforms.Unknown; + break; + } + } + + return platform; + } + } + } + #endregion // Data contract + } +} diff --git a/WindowsDevicePortalWrapper/Core/PerformanceData.cs b/WindowsDevicePortalWrapper/Core/PerformanceData.cs new file mode 100644 index 0000000..35b519c --- /dev/null +++ b/WindowsDevicePortalWrapper/Core/PerformanceData.cs @@ -0,0 +1,549 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for Performance methods + /// + public partial class DevicePortal + { + /// + /// API for getting all running processes + /// + public static readonly string RunningProcessApi = "api/resourcemanager/processes"; + + /// + /// API for getting system performance + /// + public static readonly string SystemPerfApi = "api/resourcemanager/systemperf"; + + /// + /// Web socket to get running processes. + /// + private WebSocket deviceProcessesWebSocket; + + /// + /// Web socket to get the system perf of the device. + /// + private WebSocket systemPerfWebSocket; + + /// + /// The running processes message received handler. + /// + public event WebSocketMessageReceivedEventHandler RunningProcessesMessageReceived; + + /// + /// The system perf message received handler. + /// + public event WebSocketMessageReceivedEventHandler SystemPerfMessageReceived; + + /// + /// Gets the collection of processes running on the device. + /// + /// RunningProcesses object containing the list of running processes. + public async Task GetRunningProcessesAsync() + { + return await this.GetAsync(RunningProcessApi); + } + + /// + /// Starts listening for the running processes on the device with them being returned via the RunningProcessesMessageReceived event handler. + /// + /// Task for connecting to the websocket but not for listening to it. + public async Task StartListeningForRunningProcessesAsync() + { + if (this.deviceProcessesWebSocket == null) + { +#if WINDOWS_UWP + this.deviceProcessesWebSocket = new WebSocket(this.deviceConnection); +#else + this.deviceProcessesWebSocket = new WebSocket(this.deviceConnection, this.ServerCertificateValidation); +#endif + + this.deviceProcessesWebSocket.WebSocketMessageReceived += this.RunningProcessesReceivedHandler; + } + else + { + if (this.deviceProcessesWebSocket.IsListeningForMessages) + { + return; + } + } + + await this.deviceProcessesWebSocket.ConnectAsync(RunningProcessApi); + await this.deviceProcessesWebSocket.ReceiveMessagesAsync(); + } + + /// + /// Stop listening for the running processes on the device. + /// + /// Task for stop listening for processes and disconnecting from the websocket . + public async Task StopListeningForRunningProcessesAsync() + { + await this.deviceProcessesWebSocket.CloseAsync(); + } + + /// + /// Gets system performance information for the device. + /// + /// SystemPerformanceInformation object containing information such as memory usage. + public async Task GetSystemPerfAsync() + { + return await this.GetAsync(SystemPerfApi); + } + + /// + /// Starts listening for the system performance information for the device with it being returned via the SystemPerfMessageReceived event handler. + /// + /// Task for connecting to the websocket but not for listening to it. + public async Task StartListeningForSystemPerfAsync() + { + if (this.systemPerfWebSocket == null) + { +#if WINDOWS_UWP + this.systemPerfWebSocket = new WebSocket(this.deviceConnection); +#else + this.systemPerfWebSocket = new WebSocket(this.deviceConnection, this.ServerCertificateValidation); +#endif + + this.systemPerfWebSocket.WebSocketMessageReceived += this.SystemPerfReceivedHandler; + } + else + { + if (this.systemPerfWebSocket.IsListeningForMessages) + { + return; + } + } + + await this.systemPerfWebSocket.ConnectAsync(SystemPerfApi); + await this.systemPerfWebSocket.ReceiveMessagesAsync(); + } + + /// + /// Stop listening for the system performance information for the device. + /// + /// Task for stop listening for system perf and disconnecting from the websocket . + public async Task StopListeningForSystemPerfAsync() + { + await this.systemPerfWebSocket.CloseAsync(); + } + + /// + /// Handler for the processes received event that passes the event to the RunningProcessesMessageReceived handler. + /// + /// The object sending the event. + /// The event data. + private void RunningProcessesReceivedHandler( + WebSocket sender, + WebSocketMessageReceivedEventArgs args) + { + if (args.Message != null) + { + this.RunningProcessesMessageReceived?.Invoke( + this, + args); + } + } + + /// + /// Handler for the system performance information received event that passes the event to the SystemPerfMessageReceived handler. + /// + /// The object sending the event. + /// The event data. + private void SystemPerfReceivedHandler( + WebSocket sender, + WebSocketMessageReceivedEventArgs args) + { + if (args.Message != null) + { + this.SystemPerfMessageReceived?.Invoke( + this, + args); + } + } + +#region Device contract + + /// + /// Object representing the app version. Only present if the process is an app. + /// + [DataContract] + public class AppVersion + { + /// + /// Gets the major version number + /// + [DataMember(Name = "Major")] + public uint Major { get; private set; } + + /// + /// Gets the minor version number + /// + [DataMember(Name = "Minor")] + public uint Minor { get; private set; } + + /// + /// Gets the build number + /// + [DataMember(Name = "Build")] + public uint Build { get; private set; } + + /// + /// Gets the revision number + /// + [DataMember(Name = "Revision")] + public uint Revision { get; private set; } + } + + /// + /// Process Info. Contains app information if the process is an app. + /// + [DataContract] + public class DeviceProcessInfo + { + /// + /// Gets the app name. Only present if the process is an app. + /// + [DataMember(Name = "AppName")] + public string AppName { get; private set; } + + /// + /// Gets CPU Usage as a percentage of available CPU resources (0-100) + /// + [DataMember(Name = "CPUUsage")] + public float CpuUsage { get; private set; } + + /// + /// Gets the image name + /// + [DataMember(Name = "ImageName")] + public string Name { get; private set; } + + /// + /// Gets the process id (pid) + /// + [DataMember(Name = "ProcessId")] + public uint ProcessId { get; private set; } + + /// + /// Gets the user the process is running as. + /// + [DataMember(Name = "UserName")] + public string UserName { get; private set; } + + /// + /// Gets the package full name. Only present if the process is an app. + /// + [DataMember(Name = "PackageFullName")] + public string PackageFullName { get; private set; } + + /// + /// Gets the Page file usage info, in bytes + /// + [DataMember(Name = "PageFileUsage")] + public ulong PageFile { get; private set; } + + /// + /// Gets the working set size, in bytes + /// + [DataMember(Name = "WorkingSetSize")] + public ulong WorkingSet { get; private set; } + + /// + /// Gets package working set, in bytes + /// + [DataMember(Name = "PrivateWorkingSet")] + public ulong PrivateWorkingSet { get; private set; } + + /// + /// Gets session id + /// + [DataMember(Name = "SessionId")] + public uint SessionId { get; private set; } + + /// + /// Gets total commit, in bytes + /// + [DataMember(Name = "TotalCommit")] + public ulong TotalCommit { get; private set; } + + /// + /// Gets virtual size, in bytes + /// + [DataMember(Name = "VirtualSize")] + public ulong VirtualSize { get; private set; } + + /// + /// Gets a value indicating whether or not the app is running + /// (versus suspended). Only present if the process is an app. + /// + [DataMember(Name = "IsRunning")] + public bool IsRunning { get; private set; } + + /// + /// Gets publisher. Only present if the process is an app. + /// + [DataMember(Name = "Publisher")] + public string Publisher { get; private set; } + + /// + /// Gets version. Only present if the process is an app. + /// + [DataMember(Name = "Version")] + public AppVersion Version { get; private set; } + + /// + /// Gets a value indicating whether or not the package is a XAP + /// package. Only present if the process is an app. + /// + [DataMember(Name = "IsXAP")] + public bool IsXAP { get; private set; } + + /// + /// String representation of a process + /// + /// String representation + public override string ToString() + { + return string.Format("{0} ({1})", this.AppName, this.Name); + } + } + + /// + /// GPU Adaptors + /// + [DataContract] + public class GpuAdapter + { + /// + /// Gets total Dedicated memory in bytes + /// + [DataMember(Name = "DedicatedMemory")] + public ulong DedicatedMemory { get; private set; } + + /// + /// Gets used Dedicated memory in bytes + /// + [DataMember(Name = "DedicatedMemoryUsed")] + public ulong DedicatedMemoryUsed { get; private set; } + + /// + /// Gets description + /// + [DataMember(Name = "Description")] + public string Description { get; private set; } + + /// + /// Gets system memory in bytes + /// + [DataMember(Name = "SystemMemory")] + public ulong SystemMemory { get; private set; } + + /// + /// Gets memory used in bytes + /// + [DataMember(Name = "SystemMemoryUsed")] + public ulong SystemMemoryUsed { get; private set; } + + /// + /// Gets engines utilization as percent of maximum. + /// + [DataMember(Name = "EnginesUtilization")] + public List EnginesUtilization { get; private set; } + } + + /// + /// GPU performance data + /// + [DataContract] + public class GpuPerformanceData + { + /// + /// Gets list of available adapters + /// + [DataMember(Name = "AvailableAdapters")] + public List Adapters { get; private set; } + } + + /// + /// Network performance data + /// + [DataContract] + public class NetworkPerformanceData + { + /// + /// Gets current download speed in bytes per second + /// + [DataMember(Name = "NetworkInBytes")] + public ulong BytesIn { get; private set; } + + /// + /// Gets current upload speed in bytes per second + /// + [DataMember(Name = "NetworkOutBytes")] + public ulong BytesOut { get; private set; } + } + + /// + /// Running processes + /// + [DataContract] + public class RunningProcesses + { + /// + /// Gets list of running processes. + /// + [DataMember(Name = "Processes")] + public List Processes { get; private set; } + + /// + /// Checks to see if this process Id is in the list of processes + /// + /// Process to look for + /// whether the process id was found + public bool Contains(int processId) + { + bool found = false; + + foreach (DeviceProcessInfo pi in this.Processes) + { + if (pi.ProcessId == processId) + { + found = true; + break; + } + } + + return found; + } + + /// + /// Checks for a given package name + /// + /// Name of the package to look for + /// Whether we should be case sensitive in our search + /// Whether the package was found + public bool Contains(string packageName, bool caseSensitive = true) + { + bool found = false; + + foreach (DeviceProcessInfo pi in this.Processes) + { + if (string.Compare( + pi.PackageFullName, + packageName, + caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase) == 0) + { + found = true; + break; + } + } + + return found; + } + } + + /// + /// System performance information + /// + [DataContract] + public class SystemPerformanceInformation + { + /// + /// Gets available pages + /// + [DataMember(Name = "AvailablePages")] + public ulong AvailablePages { get; private set; } + + /// + /// Gets commit limit in bytes + /// + [DataMember(Name = "CommitLimit")] + public uint CommitLimit { get; private set; } + + /// + /// Gets committed pages + /// + [DataMember(Name = "CommittedPages")] + public uint CommittedPages { get; private set; } + + /// + /// Gets CPU load as percent of maximum (0 - 100) + /// + [DataMember(Name = "CpuLoad")] + public uint CpuLoad { get; private set; } + + /// + /// Gets IO Other Speed in bytes per second + /// + [DataMember(Name = "IOOtherSpeed")] + public ulong IoOtherSpeed { get; private set; } + + /// + /// Gets IO Read speed in bytes per second. + /// + [DataMember(Name = "IOReadSpeed")] + public ulong IoReadSpeed { get; private set; } + + /// + /// Gets IO write speed in bytes per second + /// + [DataMember(Name = "IOWriteSpeed")] + public ulong IoWriteSpeed { get; private set; } + + /// + /// Gets Non paged pool pages + /// + [DataMember(Name = "NonPagedPoolPages")] + public uint NonPagedPoolPages { get; private set; } + + /// + /// Gets page size + /// + [DataMember(Name = "PageSize")] + public uint PageSize { get; private set; } + + /// + /// Gets paged pool pages + /// + [DataMember(Name = "PagedPoolPages")] + public uint PagedPoolPages { get; private set; } + + /// + /// Gets total installed in KB + /// + [DataMember(Name = "TotalInstalledInKb")] + public ulong TotalInstalledKb { get; private set; } + + /// + /// Gets total pages + /// + [DataMember(Name = "TotalPages")] + public uint TotalPages { get; private set; } + + /// + /// Gets GPU data + /// + [DataMember(Name = "GPUData")] + public GpuPerformanceData GpuData { get; private set; } + + /// + /// Gets Networking data + /// + [DataMember(Name = "NetworkingData")] + public NetworkPerformanceData NetworkData { get; private set; } + } + +#endregion // Device contract + } +} diff --git a/WindowsDevicePortalWrapper/Core/Power.cs b/WindowsDevicePortalWrapper/Core/Power.cs new file mode 100644 index 0000000..37e2e1a --- /dev/null +++ b/WindowsDevicePortalWrapper/Core/Power.cs @@ -0,0 +1,202 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for Power methods. + /// + public partial class DevicePortal + { + /// + /// API for getting or setting the active power scheme. + /// + public static readonly string ActivePowerSchemeApi = "api/power/activecfg"; + + /// + /// API for getting battery state. + /// + public static readonly string BatteryStateApi = "api/power/battery"; + + /// + /// API for getting or setting a power scheme's sub-value. + /// + public static readonly string PowerSchemeSubValueApi = "api/power/cfg"; + + /// + /// API for controlling power state. + /// + public static readonly string PowerStateApi = "api/power/state"; + + /// + /// API for getting a sleep study report. + /// + public static readonly string SleepStudyReportApi = "api/power/sleepstudy/report"; + + /// + /// API for getting the list of sleep study reports. + /// + public static readonly string SleepStudyReportsApi = "api/power/sleepstudy/reports"; + + /// + /// API for getting a sleep study report. + /// + public static readonly string SleepStudyTransformApi = "api/power/sleepstudy/transform"; + + /// + /// Returns the current active power scheme. + /// + /// The power scheme identifier. + public async Task GetActivePowerSchemeAsync() + { + ActivePowerScheme activeScheme = await this.GetAsync(ActivePowerSchemeApi); + return activeScheme.Id; + } + + /// + /// Returns the current state of the device's battery. + /// + /// BatteryState object containing details such as the current battery level. + public async Task GetBatteryStateAsync() + { + return await this.GetAsync(BatteryStateApi); + } + + /// + /// Gets the device's current power state. + /// + /// PowerState object containing details such as whether or not the device is in low power mode. + public async Task GetPowerStateAsync() + { + return await this.GetAsync(PowerStateApi); + } + + #region Data contract + + /// + /// Battery state. + /// + [DataContract] + public class ActivePowerScheme + { + /// + /// Gets the active power scheme identifier. + /// + [DataMember(Name = "ActivePowerScheme")] + public Guid Id { get; private set; } + } + + /// + /// Battery state. + /// + [DataContract] + public class BatteryState + { + /// + /// Gets a value indicating whether the device is on AC power. + /// + [DataMember(Name = "AcOnline")] + public bool IsOnAcPower { get; private set; } + + /// + /// Gets a value indicating whether a battery is present. + /// + [DataMember(Name = "BatteryPresent")] + public bool IsBatteryPresent { get; private set; } + + /// + /// Gets a value indicating whether the device is charging. + /// + [DataMember(Name = "Charging")] + public bool IsCharging { get; private set; } + + /// + /// Gets the default alert. + /// + [DataMember(Name = "DefaultAlert1")] + public int DefaultAlert1 { get; private set; } + + /// + /// Gets the default alert. + /// + [DataMember(Name = "DefaultAlert2")] + public int DefaultAlert2 { get; private set; } + + /// + /// Gets estimated battery time left in seconds. + /// + [DataMember(Name = "EstimatedTime")] + public uint EstimatedTimeRaw { get; private set; } + + /// + /// Gets maximum capacity. + /// + [DataMember(Name = "MaximumCapacity")] + public int MaximumCapacity { get; private set; } + + /// + /// Gets remaining capacity. + /// + [DataMember(Name = "RemainingCapacity")] + public int RemainingCapacity { get; private set; } + + /// + /// Gets the battery level as a percentage of the maximum capacity. + /// + public float Level + { + get + { + // Desktop PCs typically do not have a battery, return 100% + if (this.MaximumCapacity == 0) + { + return 100f; + } + + return 100.0f * ((float)this.RemainingCapacity / this.MaximumCapacity); + } + } + + /// + /// Gets the remaining battery time left, as a TimeSpan. + /// Will be 0 if the device has no battery. + /// Will be 0xFFFF,FFFF (around 138 years) if the device is charging. + /// + public TimeSpan EstimatedTime + { + get + { + return new TimeSpan(0, 0, (int)this.EstimatedTimeRaw); + } + } + } + + /// + /// Power state + /// + [DataContract] + public class PowerState + { + /// + /// Gets a value indicating whether the device is in a lower power mode + /// + [DataMember(Name = "LowPowerState")] + public bool InLowPowerState { get; private set; } + + /// + /// Gets a value indicating whether the device supports a lower power mode + /// + [DataMember(Name = "LowPowerStateAvailable")] + public bool IsLowPowerStateAvailable { get; private set; } + } + + #endregion // Data contract + } +} diff --git a/WindowsDevicePortalWrapper/Core/RemoteControl.cs b/WindowsDevicePortalWrapper/Core/RemoteControl.cs new file mode 100644 index 0000000..9ba49c2 --- /dev/null +++ b/WindowsDevicePortalWrapper/Core/RemoteControl.cs @@ -0,0 +1,48 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for Remote Control methods. + /// + public partial class DevicePortal + { + /// + /// API for rebooting the device. + /// + public static readonly string RebootApi = "api/control/restart"; + + /// + /// API for shutting down the device. + /// + public static readonly string ShutdownApi = "api/control/shutdown"; + + /// + /// Reboots the device. + /// + /// + /// Task tracking reboot completion. + /// + public async Task RebootAsync() + { + await this.PostAsync(RebootApi); + } + + /// + /// Shuts down the device. + /// + /// + /// Task tracking shutdown completion. + /// + public async Task ShutdownAsync() + { + await this.PostAsync(ShutdownApi); + } + } +} diff --git a/WindowsDevicePortalWrapper/Core/TaskManager.cs b/WindowsDevicePortalWrapper/Core/TaskManager.cs new file mode 100644 index 0000000..54af85a --- /dev/null +++ b/WindowsDevicePortalWrapper/Core/TaskManager.cs @@ -0,0 +1,69 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for Task Manager methods + /// + public partial class DevicePortal + { + /// + /// API for starting or stopping a modern application. + /// + public static readonly string TaskManagerApi = "api/taskmanager/app"; + + /// + /// Starts running the specified application. + /// + /// Application ID + /// The name of the application package. + /// Process identifier for the application instance. + public async Task LaunchApplicationAsync( + string appid, + string packageName) + { + string payload = string.Format( + "appid={0}&package={1}", + Utilities.Hex64Encode(appid), + Utilities.Hex64Encode(packageName)); + + await this.PostAsync( + TaskManagerApi, + payload); + + RunningProcesses runningApps = await this.GetRunningProcessesAsync(); + + uint processId = 0; + foreach (DeviceProcessInfo process in runningApps.Processes) + { + if (string.Compare(process.PackageFullName, packageName) == 0) + { + processId = process.ProcessId; + break; + } + } + + return processId; + } + + /// + /// Stops the specified application from running. + /// + /// The name of the application package. + /// + /// Task for tracking termination completion + /// + public async Task TerminateApplicationAsync(string packageName) + { + await this.DeleteAsync( + TaskManagerApi, + string.Format("package={0}", Utilities.Hex64Encode(packageName))); + } + } +} diff --git a/WindowsDevicePortalWrapper/Core/WiFiManagement.cs b/WindowsDevicePortalWrapper/Core/WiFiManagement.cs new file mode 100644 index 0000000..ed1063c --- /dev/null +++ b/WindowsDevicePortalWrapper/Core/WiFiManagement.cs @@ -0,0 +1,248 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for WiFi management methods. + /// + public partial class DevicePortal + { + /// + /// API for getting the WiFi interfaces. + /// + public static readonly string WifiInterfacesApi = "api/wifi/interfaces"; + + /// + /// API for the controlling the WiFi network. + /// + public static readonly string WifiNetworkApi = "api/wifi/network"; + + /// + /// API for getting available WiFi networks. + /// + public static readonly string WifiNetworksApi = "api/wifi/networks"; + + /// + /// Connect to a WiFi network using a given network adapter and SSID. + /// + /// Network adaptor GUID. + /// SSID of the network. + /// Network key. + /// Task tracking connection status. + public async Task ConnectToWifiNetworkAsync( + Guid networkAdapter, + string ssid, + string networkKey) + { + string payload = string.Format( + "interface={0}&ssid={1}&op=connect&createprofile=yes&key={2}", + networkAdapter.ToString(), + Utilities.Hex64Encode(ssid), + Utilities.Hex64Encode(networkKey)); + + await this.PostAsync( + WifiNetworkApi, + payload); + } + + /// + /// Gets WiFi interfaces. + /// + /// List of WiFi interfaces. + public async Task GetWifiInterfacesAsync() + { + return await this.GetAsync(WifiInterfacesApi); + } + + /// + /// Gets WiFi networks as seen from a WiFi interface. + /// + /// Interface to get networks from. + /// List of available networks. + public async Task GetWifiNetworksAsync(Guid interfaceGuid) + { + return await this.GetAsync( + WifiNetworksApi, + string.Format("interface={0}", interfaceGuid.ToString())); + } + + #region Data contract + + /// + /// WiFi interface. + /// + [DataContract] + public class WifiInterface + { + /// + /// Gets description. + /// + [DataMember(Name = "Description")] + public string Description { get; private set; } + + /// + /// Gets GUID. + /// + [DataMember(Name = "GUID")] + public Guid Guid { get; private set; } + + /// + /// Gets index. + /// + [DataMember(Name = "Index")] + public int Index { get; private set; } + + /// + /// Gets profiles list. + /// + [DataMember(Name = "ProfilesList")] + public List Profiles { get; private set; } + } + + /// + /// WiFi interfaces. + /// + [DataContract] + public class WifiInterfaces + { + /// + /// Gets the list of interfaces. + /// + [DataMember(Name = "Interfaces")] + public List Interfaces { get; private set; } + } + + /// + /// WiFi networks. + /// + [DataContract] + public class WifiNetworks + { + /// + /// Gets the list of available networks. + /// + [DataMember(Name = "AvailableNetworks")] + public List AvailableNetworks { get; private set; } + } + + /// + /// WiFi network info. + /// + [DataContract] + public class WifiNetworkInfo + { + /// + /// Gets a value indicating whether the device is already connected to this network. + /// + [DataMember(Name = "AlreadyConnected")] + public bool IsConnected { get; private set; } + + /// + /// Gets the authentication algorithm. + /// + [DataMember(Name = "AuthenticationAlgorithm")] + public string AuthenticationAlgorithm { get; private set; } + + /// + /// Gets the channel. + /// + [DataMember(Name = "Channel")] + public int Channel { get; private set; } + + /// + /// Gets the cipher algorithm. + /// + [DataMember(Name = "CipherAlgorithm")] + public string CipherAlgorithm { get; private set; } + + /// + /// Gets a value indicating whether this network is connectable + /// + [DataMember(Name = "Connectable")] + public bool IsConnectable { get; private set; } + + /// + /// Gets the infrastructure type - ad hoc or standard. + /// + [DataMember(Name = "InfrastructureType")] + public string InfrastructureType { get; private set; } + + /// + /// Gets a value indicating whether a profile is available. + /// + [DataMember(Name = "ProfileAvailable")] + public bool IsProfileAvailable { get; private set; } + + /// + /// Gets the profile name. + /// + [DataMember(Name = "ProfileName")] + public string ProfileName { get; private set; } + + /// + /// Gets the SSID. + /// + [DataMember(Name = "SSID")] + public string Ssid { get; private set; } + + /// + /// Gets a value indicating whether security is enabled. + /// + [DataMember(Name = "SecurityEnabled")] + public bool IsSecurityEnabled { get; private set; } + + /// + /// Gets the signal quality. + /// + [DataMember(Name = "SignalQuality")] + public int SignalQuality { get; private set; } + + /// + /// Gets the BSSID. + /// + [DataMember(Name = "BSSID")] + public List Bssid { get; private set; } + + /// + /// Gets physical types. + /// + [DataMember(Name = "PhysicalTypes")] + public List NetworkTypes { get; private set; } + } + + /// + /// WiFi network profile. + /// + [DataContract] + public class WifiNetworkProfile + { + /// + /// Gets a value indicating whether this is a group policy profile. + /// + [DataMember(Name = "GroupPolicyProfile")] + public bool IsGroupPolicyProfile { get; private set; } + + /// + /// Gets the name. + /// + [DataMember(Name = "Name")] + public string Name { get; private set; } + + /// + /// Gets a value indicating whether this is a per user profile. + /// + [DataMember(Name = "PerUserProfile")] + public bool IsPerUserProfile { get; private set; } + } + #endregion // Data contract + } +} diff --git a/WindowsDevicePortalWrapper/Core/WindowsErrorReporting.cs b/WindowsDevicePortalWrapper/Core/WindowsErrorReporting.cs new file mode 100644 index 0000000..9124a52 --- /dev/null +++ b/WindowsDevicePortalWrapper/Core/WindowsErrorReporting.cs @@ -0,0 +1,215 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +namespace Microsoft.Tools.WindowsDevicePortal +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Runtime.Serialization; + using System.Threading.Tasks; + + /// + /// Wrapper for collecting Windows Error Reports from the device. + /// + public partial class DevicePortal + { + /// + /// API for downloading a Windows error reporting file. + /// + public static readonly string WindowsErrorReportingFileApi = "api/wer/report/file"; + + /// + /// API for getting the list of files in a Windows error report. + /// + public static readonly string WindowsErrorReportingFilesApi = "api/wer/report/files"; + + /// + /// API for getting the list of Windows error reports. + /// + public static readonly string WindowsErrorReportsApi = "api/wer/reports"; + + /// + /// Gets the list of Windows Error Reporting (WER) reports. + /// + /// The list of Windows Error Reporting (WER) reports. + public async Task GetWindowsErrorReportsAsync() + { + this.CheckPlatformSupport(); + + return await this.GetAsync(WindowsErrorReportsApi); + } + + /// + /// Gets the list of files in a Windows Error Reporting (WER) report. + /// + /// The user associated with the report. + /// The type of report. This can be either 'queried' or 'archived'. + /// The name of the report. + /// The list of files. + public async Task GetWindowsErrorReportingFileListAsync(string user, string type, string name) + { + this.CheckPlatformSupport(); + + Dictionary payload = new Dictionary(); + payload.Add("user", user); + payload.Add("type", type); + payload.Add("name", Utilities.Hex64Encode(name)); + + return await this.GetAsync(WindowsErrorReportingFilesApi, Utilities.BuildQueryString(payload)); + } + + /// + /// Gets the specified file from a Windows Error Reporting (WER) report. + /// + /// The user associated with the report. + /// The type of report. This can be either 'queried' or 'archived'. + /// The name of the report. + /// The name of the file to download from the report. + /// Byte array containing the file data + public async Task GetWindowsErrorReportingFileAsync(string user, string type, string name, string file) + { + this.CheckPlatformSupport(); + + Dictionary payload = new Dictionary(); + payload.Add("user", user); + payload.Add("type", type); + payload.Add("name", Utilities.Hex64Encode(name)); + payload.Add("file", Utilities.Hex64Encode(file)); + + Uri uri = Utilities.BuildEndpoint( + this.deviceConnection.Connection, + WindowsErrorReportingFileApi, + Utilities.BuildQueryString(payload)); + + byte[] werFile = null; + using (Stream stream = await this.GetAsync(uri)) + { + werFile = new byte[stream.Length]; + stream.Read(werFile, 0, werFile.Length); + } + + return werFile; + } + + /// + /// Checks if the Windows Error Reporting (WER) APIs are being called on a supported platform. + /// + private void CheckPlatformSupport() + { + switch (this.Platform) + { + case DevicePortalPlatforms.Mobile: + case DevicePortalPlatforms.XboxOne: + throw new NotSupportedException("This method is only supported on Windows Desktop, HoloLens and IoT platforms."); + } + } + + /// + /// A list of all files contained within a Windows Error Reporting (WER) report. + /// + [DataContract] + public class WerFiles + { + /// + /// Gets a list of all files contained within a Windows Error Reporting (WER) report. + /// + [DataMember(Name = "Files")] + public List Files { get; private set; } + } + + /// + /// Information about a Windows Error Reporting (WER) report file. + /// + [DataContract] + public class WerFileInformation + { + /// + /// Gets the name of the file. + /// + [DataMember(Name = "Name")] + public string Name { get; private set; } + + /// + /// Gets the size of the file (in bytes). + /// + [DataMember(Name = "Size")] + public int Size { get; private set; } + } + + /// + /// A list of all Windows Error Reporting (WER) reports on a device. + /// + [DataContract] + public class WerDeviceReports + { + /// + /// Gets a list of all Windows Error Reporting (WER) reports on a + /// device. The SYSTEM user account usually holds the bulk of the + /// error reports. + /// + [DataMember(Name = "WerReports")] + public List UserReports { get; private set; } + + /// + /// Gets system error reports - Convenience accessor for the System error reports - this is + /// where most error reports end up. + /// + public WerUserReports SystemErrorReports + { + get + { + return this.UserReports.First(x => x.UserName == "SYSTEM"); + } + } + } + + /// + /// A list of Windows Error Reporting (WER) reports for a specific user. + /// + [DataContract] + public class WerUserReports + { + /// + /// Gets the user name. + /// + [DataMember(Name = "User")] + public string UserName { get; private set; } + + /// + /// Gets a list of Windows Error Reporting (WER) reports + /// + [DataMember(Name = "Reports")] + public List Reports { get; private set; } + } + + /// + /// Information about a Windows Error Reporting (WER) report. + /// + [DataContract] + public class WerReportInformation + { + /// + /// Gets the creation time. + /// + [DataMember(Name = "CreationTime")] + public ulong CreationTime { get; private set; } + + /// + /// Gets the report name (not base64 encoded). + /// + [DataMember(Name = "Name")] + public string Name { get; private set; } + + /// + /// Gets the report type ("Queue" or "Archive"). + /// + [DataMember(Name = "Type")] + public string Type { get; private set; } + } + } +} diff --git a/WindowsDevicePortalWrapper/Core/WindowsPerformanceRecorder.cs b/WindowsDevicePortalWrapper/Core/WindowsPerformanceRecorder.cs new file mode 100644 index 0000000..7573013 --- /dev/null +++ b/WindowsDevicePortalWrapper/Core/WindowsPerformanceRecorder.cs @@ -0,0 +1,34 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for DNS methods + /// + public partial class DevicePortal + { + /// + /// API for starting and stopping a Windows performance recorder boot performance trace session. + /// + public static readonly string WindowsPerformanceBootTraceApi = "api/wpr/boottrace"; + + /// + /// API for starting a Windows performance recorder trace using a custom profile. + /// + public static readonly string WindowsPerformanceCustomTraceApi = "api/wpr/customtrace"; + + /// + /// API for starting and stopping a Windows performance recorder trace session. + /// + public static readonly string WindowsPerformanceTraceApi = "api/wpr/trace"; + + /// + /// API for getting the status of a Windows performance recorder trace session. + /// + public static readonly string WindowsPerformanceTraceStatusApi = "api/wpr/status"; + } +} diff --git a/WindowsDevicePortalWrapper/DefaultDevicePortalConnection.cs b/WindowsDevicePortalWrapper/DefaultDevicePortalConnection.cs new file mode 100644 index 0000000..720321a --- /dev/null +++ b/WindowsDevicePortalWrapper/DefaultDevicePortalConnection.cs @@ -0,0 +1,204 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using static Microsoft.Tools.WindowsDevicePortal.DevicePortal; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Default implementation of the IDevicePortalConnection interface. + /// This implementation is designed to be compatibile with all device families. + /// + public class DefaultDevicePortalConnection : IDevicePortalConnection + { + /// + /// The device's root certificate. + /// + private X509Certificate2 deviceCertificate = null; + + /// + /// Initializes a new instance of the class. + /// + /// The fully qualified (ex: "https:/1.2.3.4:4321") address of the device. + /// The user name used in the connection credentials. + /// The password used in the connection credentials. + public DefaultDevicePortalConnection( + string address, + string userName, + string password) + { + this.Connection = new Uri(address); + + if (!string.IsNullOrEmpty(userName) && + !string.IsNullOrEmpty(password)) + { + // append auto- to the credentials to bypass CSRF token requirement on non-Get requests. + this.Credentials = new NetworkCredential(string.Format("auto-{0}", userName), password); + } + } + + /// + /// Initializes a new instance of the class, using a SecureString to secure the password. + /// + /// device identifier + /// WDP username + /// WDP password + public DefaultDevicePortalConnection( + string address, + string userName, + System.Security.SecureString password) + { + this.Connection = new Uri(address); + + if (!string.IsNullOrEmpty(userName) && + password != null && + password.Length > 0) + { + // append auto- to the credentials to bypass CSRF token requirement on non-Get requests. + this.Credentials = new NetworkCredential(string.Format("auto-{0}", userName), password); + } + } + + /// + /// Gets the URI used to connect to the device. + /// + public Uri Connection + { + get; + private set; + } + + /// + /// Gets Web Socket Connection property + /// + public Uri WebSocketConnection + { + get + { + if (this.Connection == null) + { + return null; + } + + // Convert the scheme from http[s] to ws[s]. + string scheme = this.Connection.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) ? "wss" : "ws"; + + return new Uri( + string.Format( + "{0}://{1}", + scheme, + this.Connection.Authority)); + } + } + + /// + /// Gets the credentials used to connect to the device. + /// + public NetworkCredential Credentials + { + get; + private set; + } + + /// + /// Gets or sets the device's operating system family. + /// + public string Family + { + get; + set; + } + + /// + /// Gets or sets the operating system information. + /// + public OperatingSystemInformation OsInfo + { + get; + set; + } + + /// + /// Gets the provided device certificate. + /// + /// Stored device certificate. + public X509Certificate2 GetDeviceCertificate() + { + return this.deviceCertificate; + } + + /// + /// Stores a manually provided device certificate. + /// + /// The device's root certificate. + public void SetDeviceCertificate(X509Certificate2 certificate) + { + this.deviceCertificate = certificate; + } + + /// + /// Updates the device's connection Uri. + /// + /// Indicates whether or not to always require a secure connection. + public void UpdateConnection(bool requiresHttps) + { + this.Connection = new Uri( + string.Format( + "{0}://{1}", + requiresHttps ? "https" : "http", + this.Connection.Authority)); + } + + /// + /// Updates the device's connection Uri. + /// + /// Object that describes the current network configuration. + /// True if an https connection is required, false otherwise. + /// True if the previous connection's port is to continue to be used, false otherwise. + public void UpdateConnection( + IpConfiguration ipConfig, + bool requiresHttps, + bool preservePort) + { + Uri newConnection = null; + + foreach (NetworkAdapterInfo adapter in ipConfig.Adapters) + { + foreach (IpAddressInfo addressInfo in adapter.IpAddresses) + { + // We take the first, non-169.x.x.x address we find that is not 0.0.0.0. + if ((addressInfo.Address != "0.0.0.0") && !addressInfo.Address.StartsWith("169.")) + { + string address = addressInfo.Address; + if (preservePort) + { + address = string.Format( + "{0}:{1}", + address, + this.Connection.Port); + } + + newConnection = new Uri( + string.Format( + "{0}://{1}", + requiresHttps ? "https" : "http", + address)); + break; + } + } + + if (newConnection != null) + { + this.Connection = newConnection; + break; + } + } + } + } +} diff --git a/WindowsDevicePortalWrapper/DeviceInfo.cs b/WindowsDevicePortalWrapper/DeviceInfo.cs new file mode 100644 index 0000000..bf82ecf --- /dev/null +++ b/WindowsDevicePortalWrapper/DeviceInfo.cs @@ -0,0 +1,430 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for Device Information. + /// + public partial class DevicePortal + { + /// + /// IOT device information API. + /// + public static readonly string IoTOsInfoApi = "api/iot/device/information"; + + /// + /// IOT device timezone API. + /// + public static readonly string TimezoneInfoApi = "api/iot/device/timezones"; + + /// + /// IOT device datetime API. + /// + public static readonly string DateTimeInfoApi = "api/iot/device/datetime"; + + /// + /// IOT device Controller Driver API. + /// + public static readonly string ControllerDriverApi = "api/iot/device/controllersdriver"; + + /// + /// IOT display resolution API. + /// + public static readonly string DisplayResolutionApi = "api/iot/device/displayresolution"; + + /// + /// IOT display orientation API. + /// + public static readonly string DisplayOrientationApi = "api/iot/device/displayorientation"; + + /// + /// IOT device name API. + /// + public static readonly string DeviceNameApi = "api/iot/device/name"; + + /// + /// IOT Device password API. + /// + public static readonly string ResetPasswordApi = "api/iot/device/password"; + + /// + /// IOT remote debugging pin API. + /// + public static readonly string NewRemoteDebuggingPinApi = "api/iot/device/remotedebuggingpin"; + + /// + /// IOT set timezone API. + /// + public static readonly string SetTimeZoneApi = "api/iot/device/settimezone"; + + /// + /// Gets the IoT OS Information. + /// + /// String containing the OS information. + public async Task GetIoTOSInfoAsync() + { + return await this.GetAsync(IoTOsInfoApi); + } + + /// + /// Gets the Timezone information. + /// + /// String containing the timezone information. + public async Task GetTimezoneInfoAsync() + { + return await this.GetAsync(TimezoneInfoApi); + } + + /// + /// Gets the datetime information. + /// + /// String containing the datetime information. + public async Task GetDateTimeInfoAsync() + { + return await this.GetAsync(DateTimeInfoApi); + } + + /// + /// Gets the controller driver information. + /// + /// String containing the controller driver information. + public async Task GetControllerDriverInfoAsync() + { + return await this.GetAsync(ControllerDriverApi); + } + + /// + /// Gets the dispaly orientation information. + /// + /// String containing the dispaly orientation information. + public async Task GetDisplayOrientationInfoAsync() + { + return await this.GetAsync(DisplayOrientationApi); + } + + /// + /// Gets the dispaly resolution information. + /// + /// String containing the dispaly resolution information. + public async Task GetDisplayResolutionInfoAsync() + { + return await this.GetAsync(DisplayResolutionApi); + } + + /// + /// Sets the Device Name. + /// + /// Name to set for the device. + /// Task tracking completion of the REST call. + public async Task SetIoTDeviceNameAsync(string name) + { + await this.PostAsync(DeviceNameApi, string.Format("newdevicename={0}", Utilities.Hex64Encode(name))); + } + + /// + /// Sets a new password. + /// + /// Old password. + /// New desired password. + /// Task tracking completion of the REST call. + public async Task SetNewPasswordAsync(string oldPassword, string newPassword) + { + return await this.PostAsync( + ResetPasswordApi, + string.Format("oldpassword={0}&newpassword={1}", Utilities.Hex64Encode(oldPassword), Utilities.Hex64Encode(newPassword))); + } + + /// + /// Sets a new remote debugging pin. + /// + /// New pin. + /// Task tracking completion of the REST call. + public async Task SetNewRemoteDebuggingPinAsync(string newPin) + { + await this.PostAsync( + NewRemoteDebuggingPinApi, + string.Format("newpin={0}", Utilities.Hex64Encode(newPin))); + } + + /// + /// Sets controllers drivers. + /// + /// Driver to set. + /// Task tracking completion of the REST call. + public async Task SetControllersDriversAsync(string newDriver) + { + return await this.PostAsync( + ControllerDriverApi, + string.Format("newdriver={0}", Utilities.Hex64Encode(newDriver))); + } + + /// + /// Sets Timezone. + /// + /// Timezone index. + /// Task tracking completion of the REST call. + public async Task SetTimeZoneAsync(int index) + { + return await this.PostAsync( + SetTimeZoneApi, + string.Format("index={0}", index)); + } + + /// + /// Sets display resolution. + /// + /// New display resolution. + /// Task tracking completion of the REST call. + public async Task SetDisplayResolutionAsync(string displayResolution) + { + await this.PostAsync( + DisplayResolutionApi, + string.Format("newdisplayresolution={0}", Utilities.Hex64Encode(displayResolution))); + } + + /// + /// Set display orientation. + /// + /// Desired orientation. + /// Task tracking completion of the REST call. + public async Task SetDisplayOrientationAsync(string displayOrientation) + { + await this.PostAsync( + DisplayOrientationApi, + string.Format("newdisplayorientation={0}", Utilities.Hex64Encode(displayOrientation))); + } + #region Data contract + + /// + /// Operating system information. + /// + [DataContract] + public class IoTOSInfo + { + /// + /// Gets the device model + /// + [DataMember(Name = "DeviceModel")] + public string Model { get; private set; } + + /// + /// Gets the device name. + /// + [DataMember(Name = "DeviceName")] + public string Name { get; private set; } + + /// + /// Gets the OS version + /// + [DataMember(Name = "OSVersion")] + public string OSVersion { get; private set; } + } + + /// + /// Timezone information. + /// + [DataContract] + public class TimezoneInfo + { + /// + /// Gets the current timezone + /// + [DataMember(Name = "Current")] + public Timezone CurrentTimeZone { get; private set; } + + /// + /// Gets the list of all timezones + /// + [DataMember(Name = "Timezones")] + public List Timezones { get; private set; } + } + + /// + /// Timezone specifications. + /// + [DataContract] + public partial class Timezone + { + /// + /// Gets the timezone description + /// + [DataMember(Name = "Description")] + public string Description { get; private set; } + + /// + /// Gets the timezone index + /// + [DataMember(Name = "Index")] + public int Index { get; private set; } + + /// + /// Gets the timezone name + /// + [DataMember(Name = "Name")] + public string Name { get; private set; } + } + + /// + /// DateTime information. + /// + [DataContract] + public class DateTimeInfo + { + /// + /// Gets the current date time + /// + [DataMember(Name = "Current")] + public DateTimeDescription CurrentDateTime { get; private set; } + } + + /// + /// Current Datetime information. + /// + [DataContract] + public class DateTimeDescription + { + /// + /// Gets the current day + /// + [DataMember(Name = "Day")] + public int Day { get; private set; } + + /// + /// Gets the current hour + /// + [DataMember(Name = "Hour")] + public int Hour { get; private set; } + + /// + /// Gets the current minute + /// + [DataMember(Name = "Minute")] + public int Min { get; private set; } + + /// + /// Gets the current month + /// + [DataMember(Name = "Month")] + public int Month { get; private set; } + + /// + /// Gets the current second + /// + [DataMember(Name = "Second")] + public int Sec { get; private set; } + + /// + /// Gets the current year + /// + [DataMember(Name = "Year")] + public int Year { get; private set; } + } + + /// + /// Controller Driver information. + /// + [DataContract] + public class ControllerDriverInfo + { + /// + /// Gets the current driver information + /// + [DataMember(Name = "CurrentDriver")] + public string CurrentDriver { get; private set; } + + /// + /// Gets the list of all the controller drivers information + /// + [DataMember(Name = "ControllersDrivers")] + public List ControllersDrivers { get; private set; } + + /// + /// Gets the request for reboot + /// + [DataMember(Name = "RequestReboot")] + public string RequestReboot { get; private set; } + } + + /// + /// Dispaly orientation information. + /// + [DataContract] + public class DisplayOrientationInfo + { + /// + /// Gets the dispaly orientation information + /// + [DataMember(Name = "Orientation")] + public int Orientation { get; private set; } + } + + /// + /// Dispaly resolution information. + /// + [DataContract] + public class DisplayResolutionInfo + { + /// + /// Gets the current display resolution + /// + [DataMember(Name = "Current")] + public Resolution CurrentResolution { get; private set; } + + /// + /// Gets the list of resolution specifications + /// + [DataMember(Name = "Resolutions")] + public List Resolutions { get; private set; } + } + + /// + /// Dispaly resolution specifications. + /// + [DataContract] + public class Resolution + { + /// + /// Gets the list of supported display resolutions + /// + [DataMember(Name = "Resolution")] + public string ResolutionDetail { get; private set; } + + /// + /// Gets the index for the resolution information + /// + [DataMember(Name = "Index")] + public int Index { get; private set; } + } + + /// + /// Error information if a request fails. + /// + [DataContract] + public class ErrorInformation + { + /// + /// Gets the error code + /// + [DataMember(Name = "ErrorCode")] + public int ErrorCode { get; private set; } + + /// + /// Gets the status of the request + /// + [DataMember(Name = "Status")] + public string Status { get; private set; } + } + + #endregion // Data contract + } +} diff --git a/WindowsDevicePortalWrapper/DevicePortal.cs b/WindowsDevicePortalWrapper/DevicePortal.cs new file mode 100644 index 0000000..259c3a0 --- /dev/null +++ b/WindowsDevicePortalWrapper/DevicePortal.cs @@ -0,0 +1,496 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +#if !WINDOWS_UWP +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +#endif // !WINDOWS_UWP +#if WINDOWS_UWP +using System.Runtime.InteropServices.WindowsRuntime; +#endif // WINDOWS_UWP +#if !WINDOWS_UWP +using System.Security.Cryptography.X509Certificates; +#endif // !WINDOWS_UWP +using System.Threading; +using System.Threading.Tasks; +#if WINDOWS_UWP +using Windows.Security.Cryptography.Certificates; +using Windows.Web.Http; +using Windows.Web.Http.Headers; +#endif // WINDOWS_UWP + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// This is the main DevicePortal object. It contains methods for making HTTP REST calls against + /// all of the WDP endpoints covered by the wrapper project. Different endpoints have their + /// implementation separated out into individual files. + /// + public partial class DevicePortal + { + /// + /// Issuer for the device certificate. + /// + public static readonly string DevicePortalCertificateIssuer = "Microsoft Windows Web Management"; + + /// + /// Endpoint used to access the certificate. + /// + public static readonly string RootCertificateEndpoint = "config/rootcertificate"; + + /// + /// Expected number of OS version sections once the OS version is split by period characters + /// + private static readonly uint ExpectedOSVersionSections = 5; + + /// + /// The target OS version section index once the OS version is split by periods + /// + private static readonly uint TargetOSVersionSection = 3; + + /// + /// Device connection object. + /// + private IDevicePortalConnection deviceConnection; +#if !WINDOWS_UWP + + /// + /// Initializes static members of the class. + /// + static DevicePortal() + { + System.Net.ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls; + } + +#endif + /// + /// Initializes a new instance of the class. + /// + /// Implementation of a connection object. + public DevicePortal(IDevicePortalConnection connection) + { + this.deviceConnection = connection; + } + + /// + /// Handler for reporting connection status. + /// + public event DeviceConnectionStatusEventHandler ConnectionStatus; + + /// + /// HTTP Methods + /// + public enum HttpMethods + { + /// + /// The HTTP Get method. + /// + Get, + + /// + /// The HTTP Put method. + /// + Put, + + /// + /// The HTTP Post method. + /// + Post, + + /// + /// The HTTP Delete method. + /// + Delete, + + /// + /// The HTTP WebSocket method. + /// + WebSocket + } + + /// + /// Gets the device address. + /// + public string Address + { + get { return this.deviceConnection.Connection.Authority; } + } + + /// + /// Gets the status code for establishing our connection. + /// + public HttpStatusCode ConnectionHttpStatusCode + { + get; + private set; + } + + /// + /// Gets a description of why the connection failed. + /// + public string ConnectionFailedDescription + { + get; + private set; + } + + /// + /// Gets the device operating system family. + /// + public string DeviceFamily + { + get + { + return (this.deviceConnection.Family != null) ? this.deviceConnection.Family : string.Empty; + } + } + + /// + /// Gets the operating system version. + /// + public string OperatingSystemVersion + { + get + { + return (this.deviceConnection.OsInfo != null) ? this.deviceConnection.OsInfo.OsVersionString : string.Empty; + } + } + + /// + /// Gets the device platform. + /// + public DevicePortalPlatforms Platform + { + get + { + return (this.deviceConnection.OsInfo != null) ? this.deviceConnection.OsInfo.Platform : DevicePortalPlatforms.Unknown; + } + } + + /// + /// Gets the device platform name. + /// + public string PlatformName + { + get + { + return (this.deviceConnection.OsInfo != null) ? this.deviceConnection.OsInfo.PlatformName : "Unknown"; + } + } + + /// + /// Connects to the device pointed to by IDevicePortalConnection provided in the constructor. + /// + /// Optional network SSID. + /// Optional network key. + /// Indicates whether we should update this connection's IP address after connecting. + /// A manually provided X509 Certificate for trust validation against this device. + /// Connect sends ConnectionStatus events to indicate the current progress in the connection process. + /// Some applications may opt to not register for the ConnectionStatus event and await on Connect. + /// Task for tracking the connect. + [method: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:ParameterMustNotSpanMultipleLines", Justification = "manualCertificate param doesn't really span multiple lines, it just has a different type for UWP and .NET implementations.")] + public async Task ConnectAsync( + string ssid = null, + string ssidKey = null, + bool updateConnection = false, +#if WINDOWS_UWP + Certificate manualCertificate = null) +#else + X509Certificate2 manualCertificate = null) +#endif + { +#if WINDOWS_UWP + this.ConnectionHttpStatusCode = HttpStatusCode.Ok; +#else + this.ConnectionHttpStatusCode = HttpStatusCode.OK; +#endif // WINDOWS_UWP + string connectionPhaseDescription = string.Empty; + + if (manualCertificate != null) + { + this.SetManualCertificate(manualCertificate); + } + + try + { + // Get the device family and operating system information. + connectionPhaseDescription = "Requesting operating system information"; + this.SendConnectionStatus( + DeviceConnectionStatus.Connecting, + DeviceConnectionPhase.RequestingOperatingSystemInformation, + connectionPhaseDescription); + this.deviceConnection.Family = await this.GetDeviceFamilyAsync().ConfigureAwait(false); + this.deviceConnection.OsInfo = await this.GetOperatingSystemInformationAsync().ConfigureAwait(false); + + // Default to using whatever was specified in the connection. + bool requiresHttps = this.IsUsingHttps(); + + // HoloLens is the only device that supports the GetIsHttpsRequired method. + if (this.deviceConnection.OsInfo.Platform == DevicePortalPlatforms.HoloLens) + { + // Check to see if HTTPS is required to communicate with this device. + connectionPhaseDescription = "Checking secure connection requirements"; + this.SendConnectionStatus( + DeviceConnectionStatus.Connecting, + DeviceConnectionPhase.DeterminingConnectionRequirements, + connectionPhaseDescription); + requiresHttps = await this.GetIsHttpsRequiredAsync().ConfigureAwait(false); + } + + // Connect the device to the specified network. + if (!string.IsNullOrWhiteSpace(ssid)) + { + connectionPhaseDescription = string.Format("Connecting to {0} network", ssid); + this.SendConnectionStatus( + DeviceConnectionStatus.Connecting, + DeviceConnectionPhase.ConnectingToTargetNetwork, + connectionPhaseDescription); + WifiInterfaces wifiInterfaces = await this.GetWifiInterfacesAsync().ConfigureAwait(false); + + // TODO - consider what to do if there is more than one wifi interface on a device + await this.ConnectToWifiNetworkAsync(wifiInterfaces.Interfaces[0].Guid, ssid, ssidKey).ConfigureAwait(false); + } + + // Get the device's IP configuration and update the connection as appropriate. + if (updateConnection) + { + connectionPhaseDescription = "Updating device connection"; + this.SendConnectionStatus( + DeviceConnectionStatus.Connecting, + DeviceConnectionPhase.UpdatingDeviceAddress, + connectionPhaseDescription); + + bool preservePort = true; + + // HoloLens and Mobile are the only devices that support USB. + // They require the port to be changed when the connection is updated + // to WiFi. + if ((this.Platform == DevicePortalPlatforms.HoloLens) || + (this.Platform == DevicePortalPlatforms.Mobile)) + { + preservePort = false; + } + + this.deviceConnection.UpdateConnection( + await this.GetIpConfigAsync().ConfigureAwait(false), + requiresHttps, + preservePort); + } + + this.SendConnectionStatus( + DeviceConnectionStatus.Connected, + DeviceConnectionPhase.Idle, + "Device connection established"); + } + catch (Exception e) + { + DevicePortalException dpe = e as DevicePortalException; + + if (dpe != null) + { + this.ConnectionHttpStatusCode = dpe.StatusCode; + this.ConnectionFailedDescription = dpe.Message; + } + else + { + this.ConnectionHttpStatusCode = HttpStatusCode.Conflict; + + // Get to the innermost exception for our return message. + Exception innermostException = e; + while (innermostException.InnerException != null) + { + innermostException = innermostException.InnerException; + await Task.Yield(); + } + + this.ConnectionFailedDescription = innermostException.Message; + } + + this.SendConnectionStatus( + DeviceConnectionStatus.Failed, + DeviceConnectionPhase.Idle, + string.Format("Device connection failed: {0}, {1}", connectionPhaseDescription, this.ConnectionFailedDescription)); + } + } + + /// + /// Helper method used for saving the content of a response to a file. + /// This allows unittests to easily generate real data to use as mock responses. + /// + /// API endpoint we are calling. + /// Directory to store our file. + /// The http method to be performed. + /// An optional stream to use for the request body content. + /// The content type of the request stream. + /// Task waiting for HTTP call to return and file copy to complete. + public async Task SaveEndpointResponseToFileAsync( + string endpoint, + string directory, + HttpMethods httpMethod, + Stream requestBody = null, + string requestBodyContentType = null) + { + Uri uri = new Uri(this.deviceConnection.Connection, endpoint); + + // Convert the OS version, such as 14385.1002.amd64fre.rs1_xbox_rel_1608.160709-1700, into a friendly OS version, such as rs1_xbox_rel_1608 + string friendlyOSVersion = this.OperatingSystemVersion; + string[] versionParts = friendlyOSVersion.Split('.'); + if (versionParts.Length == ExpectedOSVersionSections) + { + friendlyOSVersion = versionParts[TargetOSVersionSection]; + } + + // Create the filename as DeviceFamily_OSVersion.dat, replacing '/', '.', and '-' with '_' so + // we can create a class with the same name as this Device/OS pair for tests. + string filename = endpoint + "_" + this.Platform.ToString() + "_" + friendlyOSVersion; + + if (httpMethod != HttpMethods.Get) + { + filename = httpMethod.ToString() + "_" + filename; + } + + Utilities.ModifyEndpointForFilename(ref filename); + + filename += ".dat"; + string filepath = Path.Combine(directory, filename); + + if (HttpMethods.WebSocket == httpMethod) + { +#if WINDOWS_UWP + WebSocket websocket = new WebSocket(this.deviceConnection, true); +#else + WebSocket websocket = new WebSocket(this.deviceConnection, this.ServerCertificateValidation, true); +#endif // WINDOWS_UWP + + ManualResetEvent streamReceived = new ManualResetEvent(false); + Stream stream = null; + + WindowsDevicePortal.WebSocketStreamReceivedEventInternalHandler streamReceivedHandler = + delegate(WebSocket sender, WebSocketMessageReceivedEventArgs args) + { + if (args.Message != null) + { + stream = args.Message; + streamReceived.Set(); + } + }; + + websocket.WebSocketStreamReceived += streamReceivedHandler; + + await websocket.ConnectAsync(endpoint); + + await websocket.ReceiveMessagesAsync(); + + streamReceived.WaitOne(); + + await websocket.CloseAsync(); + + websocket.WebSocketStreamReceived -= streamReceivedHandler; + + using (stream) + { + using (var fileStream = File.Create(filepath)) + { + stream.Seek(0, SeekOrigin.Begin); + stream.CopyTo(fileStream); + } + } + } + else if (HttpMethods.Put == httpMethod) + { +#if WINDOWS_UWP + HttpStreamContent streamContent = null; +#else + StreamContent streamContent = null; +#endif // WINDOWS_UWP + + if (requestBody != null) + { +#if WINDOWS_UWP + streamContent = new HttpStreamContent(requestBody.AsInputStream()); + streamContent.Headers.ContentType = new HttpMediaTypeHeaderValue(requestBodyContentType); +#else + streamContent = new StreamContent(requestBody); + streamContent.Headers.ContentType = new MediaTypeHeaderValue(requestBodyContentType); +#endif // WINDOWS_UWP + } + + using (Stream dataStream = await this.PutAsync(uri, streamContent)) + { + using (var fileStream = File.Create(filepath)) + { + dataStream.Seek(0, SeekOrigin.Begin); + dataStream.CopyTo(fileStream); + } + } + } + else if (HttpMethods.Post == httpMethod) + { + using (Stream dataStream = await this.PostAsync(uri, requestBody, requestBodyContentType)) + { + using (var fileStream = File.Create(filepath)) + { + dataStream.Seek(0, SeekOrigin.Begin); + dataStream.CopyTo(fileStream); + } + } + } + else if (HttpMethods.Delete == httpMethod) + { + using (Stream dataStream = await this.DeleteAsync(uri)) + { + using (var fileStream = File.Create(filepath)) + { + dataStream.Seek(0, SeekOrigin.Begin); + dataStream.CopyTo(fileStream); + } + } + } + else if (HttpMethods.Get == httpMethod) + { + using (Stream dataStream = await this.GetAsync(uri)) + { + using (var fileStream = File.Create(filepath)) + { + dataStream.Seek(0, SeekOrigin.Begin); + dataStream.CopyTo(fileStream); + } + } + } + else + { + throw new NotImplementedException(string.Format("Unsupported HttpMethod {0}", httpMethod.ToString())); + } + } + + /// + /// Sends the connection status back to the caller + /// + /// Status of the connect attempt. + /// Current phase of the connection attempt. + /// Optional message describing the connection status. + private void SendConnectionStatus( + DeviceConnectionStatus status, + DeviceConnectionPhase phase, + string message = "") + { + this.ConnectionStatus?.Invoke(this, new DeviceConnectionStatusEventArgs(status, phase, message)); + } + + /// + /// Helper method to determine if our connection is using HTTPS + /// + /// Whether we are using HTTPS + private bool IsUsingHttps() + { + return this.deviceConnection.Connection.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/WindowsDevicePortalWrapper/Events/ApplicationInstallStatus.cs b/WindowsDevicePortalWrapper/Events/ApplicationInstallStatus.cs new file mode 100644 index 0000000..ff1ff47 --- /dev/null +++ b/WindowsDevicePortalWrapper/Events/ApplicationInstallStatus.cs @@ -0,0 +1,106 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +[module: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:FileHeaderFileNameDocumentationMustMatchTypeName", Justification = "Filename matches the enum better than the ApplicationInstallStatusEventArgs class.")] + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Application install status event handler + /// + /// sender object + /// install args + public delegate void ApplicationInstallStatusEventHandler(DevicePortal sender, ApplicationInstallStatusEventArgs args); + + /// + /// Application install status + /// + public enum ApplicationInstallStatus + { + /// + /// No install status + /// + None, + + /// + /// Installation is in progress + /// + InProgress, + + /// + /// Installation is completed + /// + Completed, + + /// + /// Installation failed + /// + Failed + } + + /// + /// Install phase + /// + public enum ApplicationInstallPhase + { + /// + /// Idle phase + /// + Idle, + + /// + /// Uninstalling the previous version + /// + UninstallingPreviousVersion, + + /// + /// Copying the package file + /// + CopyingFile, + + /// + /// Installing the package + /// + Installing + } + + /// + /// Application install status event args + /// + public class ApplicationInstallStatusEventArgs : System.EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// Install status + /// Install phase + /// Install message + internal ApplicationInstallStatusEventArgs( + ApplicationInstallStatus status, + ApplicationInstallPhase phase, + string message = "") + { + this.Status = status; + this.Phase = phase; + this.Message = message; + } + + /// + /// Gets the install status + /// + public ApplicationInstallStatus Status { get; private set; } + + /// + /// Gets the install phase + /// + public ApplicationInstallPhase Phase { get; private set; } + + /// + /// Gets the install message + /// + public string Message { get; private set; } + } +} diff --git a/WindowsDevicePortalWrapper/Events/ConnectionStatus.cs b/WindowsDevicePortalWrapper/Events/ConnectionStatus.cs new file mode 100644 index 0000000..74d22d3 --- /dev/null +++ b/WindowsDevicePortalWrapper/Events/ConnectionStatus.cs @@ -0,0 +1,116 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +[module: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:FileHeaderFileNameDocumentationMustMatchTypeName", Justification = "Filename matches the enum better than the EventArgs class.")] + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Handler for reporting on device connection status + /// + /// sender object + /// connection status details + public delegate void DeviceConnectionStatusEventHandler(DevicePortal sender, DeviceConnectionStatusEventArgs args); + + /// + /// Connection status enumeration + /// + public enum DeviceConnectionStatus + { + /// + /// No status + /// + None, + + /// + /// Currently Connecting + /// + Connecting, + + /// + /// Connection complete + /// + Connected, + + /// + /// Connection failed + /// + Failed, + } + + /// + /// Connection phase enumeration + /// + public enum DeviceConnectionPhase + { + /// + /// Idle phase + /// + Idle, + + /// + /// Acquiring the device certificate + /// + AcquiringCertificate, + + /// + /// Determining the device connection requirements + /// + DeterminingConnectionRequirements, + + /// + /// Getting some basic information about the device OS + /// + RequestingOperatingSystemInformation, + + /// + /// Connecting the device to a provided network + /// + ConnectingToTargetNetwork, + + /// + /// Updating the device address to reflect the new network + /// + UpdatingDeviceAddress + } + + /// + /// Contains the details about the connection status + /// + public class DeviceConnectionStatusEventArgs : System.EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// Status of the connection + /// Phase of the connection + /// Optional message describing our connection/phase + internal DeviceConnectionStatusEventArgs( + DeviceConnectionStatus status, + DeviceConnectionPhase phase, + string message = "") + { + this.Status = status; + this.Phase = phase; + this.Message = message; + } + + /// + /// Gets the status of the connection attempt + /// + public DeviceConnectionStatus Status { get; private set; } + + /// + /// Gets the phase of the connection + /// + public DeviceConnectionPhase Phase { get; private set; } + + /// + /// Gets a message describing the connection status/phase + /// + public string Message { get; private set; } + } +} diff --git a/WindowsDevicePortalWrapper/Events/WebSocketMessageReceivedEventArgs.cs b/WindowsDevicePortalWrapper/Events/WebSocketMessageReceivedEventArgs.cs new file mode 100644 index 0000000..93ad65b --- /dev/null +++ b/WindowsDevicePortalWrapper/Events/WebSocketMessageReceivedEventArgs.cs @@ -0,0 +1,40 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System.IO; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Web socket message received event handler + /// + /// Sender object + /// Web socket message received args + /// Return type for the websocket messages. + public delegate void WebSocketMessageReceivedEventHandler(DevicePortal sender, WebSocketMessageReceivedEventArgs args); + + /// + /// Web socket message received event args + /// + /// Return type for the websocket messages. + public class WebSocketMessageReceivedEventArgs : System.EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The message from the web socket. + internal WebSocketMessageReceivedEventArgs( + T message) + { + this.Message = message; + } + + /// + /// Gets the web socket message + /// + public T Message { get; private set; } + } +} diff --git a/WindowsDevicePortalWrapper/Exceptions/DevicePortalException.cs b/WindowsDevicePortalWrapper/Exceptions/DevicePortalException.cs new file mode 100644 index 0000000..be9154a --- /dev/null +++ b/WindowsDevicePortalWrapper/Exceptions/DevicePortalException.cs @@ -0,0 +1,238 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.IO; +#if !WINDOWS_UWP +using System.Net; +using System.Net.Http; +#endif // !WINDOWS_UWP +using System.Runtime.InteropServices.WindowsRuntime; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Security; +using System.Threading.Tasks; +#if WINDOWS_UWP +using Windows.Foundation; +using Windows.Storage.Streams; +using Windows.Web.Http; +#endif // WINDOWS_UWP + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Base exception class for a Device Portal exception + /// +#if !WINDOWS_UWP + [Serializable] +#endif // !WINDOWS_UWP + public class DevicePortalException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The Http status code. + /// Http parsed error response message. + /// Request URI which threw the exception. + /// Optional exception message. + /// Optional inner exception. + public DevicePortalException( + HttpStatusCode statusCode, + HttpErrorResponse errorResponse, + Uri requestUri = null, + string message = "", + Exception innerException = null) : this( + statusCode, + errorResponse.Reason, + requestUri, + message, + innerException) + { + this.HResult = errorResponse.ErrorCode; + this.Reason = errorResponse.ErrorMessage; + + // If we didn't get the Hresult and reason from these properties, try the other ones. + if (this.HResult == 0) + { + this.HResult = errorResponse.Code; + } + + if (string.IsNullOrEmpty(this.Reason)) + { + this.Reason = errorResponse.Reason; + } + } + + /// + /// Initializes a new instance of the class. + /// + /// Http status code. + /// Reason for exception. + /// Request URI which threw the exception. + /// Optional message. + /// Optional inner exception. + public DevicePortalException( + HttpStatusCode statusCode, + string reason, + Uri requestUri = null, + string message = "", + Exception innerException = null) : base( + message, + innerException) + { + this.StatusCode = statusCode; + this.Reason = reason; + this.RequestUri = requestUri; + } + + /// + /// Gets the HTTP Status code. + /// + public HttpStatusCode StatusCode { get; private set; } + + /// + /// Gets a reason for the exception. + /// + public string Reason { get; private set; } + + /// + /// Gets the request URI that threw the exception. + /// + public Uri RequestUri { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// Http response message. + /// Optional exception message. + /// Optional inner exception. + /// async task + public static async Task CreateAsync( + HttpResponseMessage responseMessage, + string message = "", + Exception innerException = null) + { + DevicePortalException error = new DevicePortalException( + responseMessage.StatusCode, + responseMessage.ReasonPhrase, + responseMessage.RequestMessage != null ? responseMessage.RequestMessage.RequestUri : null, + message, + innerException); + try + { + if (responseMessage.Content != null) + { + Stream dataStream = null; +#if !WINDOWS_UWP + using (HttpContent content = responseMessage.Content) + { + dataStream = new MemoryStream(); + + await content.CopyToAsync(dataStream).ConfigureAwait(false); + + // Ensure we point the stream at the origin. + dataStream.Position = 0; + } +#else // WINDOWS_UWP + IBuffer dataBuffer = null; + using (IHttpContent messageContent = responseMessage.Content) + { + dataBuffer = await messageContent.ReadAsBufferAsync(); + + if (dataBuffer != null) + { + dataStream = dataBuffer.AsStream(); + } + } +#endif // WINDOWS_UWP + + if (dataStream != null) + { + HttpErrorResponse errorResponse = DevicePortal.ReadJsonStream(dataStream); + + if (errorResponse != null) + { + error.HResult = errorResponse.ErrorCode; + error.Reason = errorResponse.ErrorMessage; + + // If we didn't get the Hresult and reason from these properties, try the other ones. + if (error.HResult == 0) + { + error.HResult = errorResponse.Code; + } + + if (string.IsNullOrEmpty(error.Reason)) + { + error.Reason = errorResponse.Reason; + } + } + } + } + } + catch (Exception) + { + // Do nothing if we fail to get additional error details from the response body. + } + + return error; + } + +#if !WINDOWS_UWP + /// + /// Get object data override + /// + /// Serialization info + /// Streaming context + [SecurityCritical] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + } +#endif // !WINDOWS_UWP + + #region data contract + + /// + /// Object containing additional error information from + /// an HTTP response. + /// + [DataContract] + public class HttpErrorResponse + { + /// + /// Gets the ErrorCode + /// + [DataMember(Name = "ErrorCode")] + public int ErrorCode { get; private set; } + + /// + /// Gets the Code (used by some endpoints instead of ErrorCode). + /// + [DataMember(Name = "Code")] + public int Code { get; private set; } + + /// + /// Gets the ErrorMessage + /// + [DataMember(Name = "ErrorMessage")] + public string ErrorMessage { get; private set; } + + /// + /// Gets the Reason (used by some endpoints instead of ErrorMessage). + /// + [DataMember(Name = "Reason")] + public string Reason { get; private set; } + + /// + /// Gets a value indicating whether the operation succeeded. For an error this should generally be false if present. + /// + [DataMember(Name = "Success")] + public bool Success { get; private set; } + } + + #endregion + } +} diff --git a/WindowsDevicePortalWrapper/HoloLens/HolographicOs.cs b/WindowsDevicePortalWrapper/HoloLens/HolographicOs.cs new file mode 100644 index 0000000..199784a --- /dev/null +++ b/WindowsDevicePortalWrapper/HoloLens/HolographicOs.cs @@ -0,0 +1,270 @@ +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for Holographic OS methods + /// + public partial class DevicePortal + { + /// + /// API for getting or setting Interpupilary distance + /// + public static readonly string HolographicIpdApi = "api/holographic/os/settings/ipd"; + + /// + /// API for getting a list of running HoloLens specific services. + /// + public static readonly string HolographicServicesApi = "api/holographic/os/services"; + + /// + /// API for getting or setting HTTPS setting + /// + public static readonly string HolographicWebManagementHttpSettingsApi = "api/holographic/os/webmanagement/settings/https"; + + /// + /// Enumeration describing the status of a process + /// + public enum ProcessStatus + { + /// + /// The process is running + /// + Running = 0, + + /// + /// The process is stopped + /// + Stopped + } + + /// + /// Gets the status of the Holographic Services on this HoloLens. + /// + /// HolographicServices object describing the state of the Holographic services. + /// This method is only supported on HoloLens. + public async Task GetHolographicServiceState() + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + return await this.GetAsync(HolographicServicesApi); + } + + /// + /// Gets the interpupilary distance registered on the device. + /// + /// Interpupilary distance, in millimeters. + /// This method is only supported on HoloLens. + public async Task GetInterPupilaryDistanceAsync() + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + InterPupilaryDistance ipd = await this.GetAsync(HolographicIpdApi); + return ipd.Ipd; + } + + /// + /// Sets the WiFi http security requirements for communication with the device. + /// + /// Desired value for HTTPS communication + /// True if WiFi based communication requires a secure connection, false otherwise. + /// This method is only supported on HoloLens. + public async Task SetIsHttpsRequiredAsync(bool httpsRequired) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + await this.PostAsync( + HolographicWebManagementHttpSettingsApi, + string.Format("required={0}", httpsRequired)); + + this.deviceConnection.UpdateConnection(httpsRequired); + } + + /// + /// Sets the interpupilary distance registered on the device. + /// + /// Interpupilary distance, in millimeters. + /// Task for tracking the POST call + /// This method is only supported on HoloLens. + public async Task SetInterPupilaryDistanceAsync(float ipd) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + string payload = string.Format("ipd={0}", (int)(ipd * 1000.0f)); + + await this.PostAsync( + HolographicIpdApi, + payload); + } + + /// + /// Gets the WiFi http security requirements for communication with the device. + /// + /// True if WiFi based communication requires a secure connection, false otherwise. + /// This method is only supported on HoloLens. + public async Task GetIsHttpsRequiredAsync() + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + WebManagementHttpSettings httpSettings = await this.GetAsync(HolographicWebManagementHttpSettingsApi); + return httpSettings.IsHttpsRequired; + } + + #region Data contract + /// + /// Object reporesentation of the status of the Holographic services + /// + [DataContract] + public class HolographicServices + { + /// + /// Gets the status for the collection of holographic services + /// + [DataMember(Name = "SoftwareStatus")] + public HolographicSoftwareStatus Status { get; private set; } + } + + /// + /// Object representation of the collection of holographic services. + /// + [DataContract] + public class HolographicSoftwareStatus + { + /// + /// Gets the status of dwm.exe + /// + [DataMember(Name = "dwm.exe")] + public ServiceStatus Dwm { get; private set; } + + /// + /// Gets the status of holoshellapp.exe + /// + [DataMember(Name = "holoshellapp.exe")] + public ServiceStatus HoloShellApp { get; private set; } + + /// + /// Gets the status of holosi.exe + /// + [DataMember(Name = "holosi.exe")] + public ServiceStatus HoloSi { get; private set; } + + /// + /// Gets the status of mixedrealitycapture.exe + /// + [DataMember(Name = "mixedrealitycapture.exe")] + public ServiceStatus MixedRealitytCapture { get; private set; } + + /// + /// Gets the status of sihost.exe + /// + [DataMember(Name = "sihost.exe")] + public ServiceStatus SiHost { get; private set; } + + /// + /// Gets the status of spectrum.exe + /// + [DataMember(Name = "spectrum.exe")] + public ServiceStatus Spectrum { get; private set; } + } + + /// + /// Object representation for Interpupilary distance + /// + [DataContract] + public class InterPupilaryDistance + { + /// + /// Gets the raw interpupilary distance + /// + [DataMember(Name = "ipd")] + public int IpdRaw { get; private set; } + + /// + /// Gets or sets the interpupilary distance + /// + public float Ipd + { + get { return this.IpdRaw / 1000.0f; } + set { this.IpdRaw = (int)(value * 1000); } + } + } + + /// + /// Object representation of the status of a service + /// + [DataContract] + public class ServiceStatus + { + /// + /// Gets the raw value returned for the expected service status + /// + [DataMember(Name = "Expected")] + public string ExpectedRaw { get; private set; } + + /// + /// Gets the raw value returned for the observed service status + /// + [DataMember(Name = "Observed")] + public string ObservedRaw { get; private set; } + + /// + /// Gets the the expected service status + /// + public ProcessStatus Expected + { + get + { + return (this.ExpectedRaw == "Running") ? ProcessStatus.Running : ProcessStatus.Stopped; + } + } + + /// + /// Gets the the observed service status + /// + public ProcessStatus Observed + { + get + { + return (this.ObservedRaw == "Running") ? ProcessStatus.Running : ProcessStatus.Stopped; + } + } + } + + /// + /// Object representation for HTTP settings + /// + [DataContract] + public class WebManagementHttpSettings + { + /// + /// Gets a value indicating whether HTTPS is required + /// + [DataMember(Name = "httpsRequired")] + public bool IsHttpsRequired { get; private set; } + } + + #endregion // Data contract + } +} diff --git a/WindowsDevicePortalWrapper/HoloLens/HolographicPerception.cs b/WindowsDevicePortalWrapper/HoloLens/HolographicPerception.cs new file mode 100644 index 0000000..8184998 --- /dev/null +++ b/WindowsDevicePortalWrapper/HoloLens/HolographicPerception.cs @@ -0,0 +1,195 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for Holographic Perception methods + /// + public partial class DevicePortal + { + /// + /// API for running a Perception client. + /// + public static readonly string HolographicPerceptionClient = "api/holographic/perception/client"; + + /// + /// API for getting or setting the Holographic Perception Simulation control mode. + /// + public static readonly string HolographicSimulationModeApi = "api/holographic/simulation/control/mode"; + + /// + /// API for controlling the Holographic Perception Simulation control stream. + /// + public static readonly string HolographicSimulationStreamApi = "api/holographic/simulation/control/stream"; + + /// + /// Enumeration defining the control modes used by the Holographic Perception Simulation. + /// + public enum SimulationControlMode + { + /// + /// Default mode. + /// + Default = 0, + + /// + /// Simulation mode. + /// + Simulation + } + + /// + /// Enumeration defining the priority levels for the Holographic Perception Simulation control stream. + /// + public enum SimulationControlStreamPriority + { + /// + /// Low priority. + /// + Low = 0, + + /// + /// Normal priority. + /// + Normal + } + + /// + /// Creates a simulation control stream. + /// + /// The control stream priority. + /// The identifier of the created stream. + /// This method is only supported on HoloLens. + public async Task CreatePerceptionSimulationControlStreamAsync(SimulationControlStreamPriority priority) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + if (!(await this.VerifySimulationControlModeAsync(SimulationControlMode.Simulation))) + { + throw new InvalidOperationException("The simulation control mode on the target HoloLens must be 'Simulation'."); + } + + string payload = string.Format( + "priority={0}", + (int)priority); + + PerceptionSimulationControlStreamId controlStreamId = await this.GetAsync( + HolographicSimulationStreamApi, + payload); + + return controlStreamId.StreamId; + } + + /// + /// Deletes a simulation control stream. + /// + /// The identifier of the stream to be deleted. + /// Task tracking completion of the REST call. + /// This method is only supported on HoloLens. + public async Task DeletePerceptionSimulationControlStreamAsync(string streamId) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + if (!(await this.VerifySimulationControlModeAsync(SimulationControlMode.Simulation))) + { + throw new InvalidOperationException("The simulation control mode on the target HoloLens must be 'Simulation'."); + } + + string payload = string.Format( + "streamId={0}", + streamId); + + await this.DeleteAsync( + HolographicSimulationStreamApi, + payload); + } + + /// + /// Gets the perception simulation control mode. + /// + /// The simulation control mode. + /// This method is only supported on HoloLens. + public async Task GetPerceptionSimulationControlModeAsync() + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + PerceptionSimulationControlMode controlMode = await this.GetAsync(HolographicSimulationModeApi); + return controlMode.Mode; + } + + /// + /// Sets the perception simulation control mode. + /// + /// The simulation control mode. + /// Task tracking completion of the REST call. + /// This method is only supported on HoloLens. + public async Task SetPerceptionSimulationControlModeAsync(SimulationControlMode mode) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + string payload = string.Format( + "mode={0}", + (int)mode); + await this.PostAsync(HolographicSimulationModeApi, payload); + } + + /// + /// Compares the current simulation control mode with the expected mode. + /// + /// The simulation control mode that we expect the device to be in. + /// The simulation control mode. + private async Task VerifySimulationControlModeAsync(SimulationControlMode expectedMode) + { + SimulationControlMode simMode = await this.GetPerceptionSimulationControlModeAsync(); + return simMode == expectedMode; + } + + #region Data contract + /// + /// Object representation of Perception Simulation control mode. + /// + [DataContract] + public class PerceptionSimulationControlMode + { + /// + /// Gets the control mode. + /// + [DataMember(Name = "mode")] + public SimulationControlMode Mode { get; private set; } + } + + /// + /// Object representation of the response recevied when creating a Perception Simulation control stream. + /// + [DataContract] + public class PerceptionSimulationControlStreamId + { + /// + /// Gets the stream identifier. + /// + [DataMember(Name = "streamId")] + public string StreamId { get; private set; } + } + #endregion // Data contract + } +} diff --git a/WindowsDevicePortalWrapper/HoloLens/HolographicThermal.cs b/WindowsDevicePortalWrapper/HoloLens/HolographicThermal.cs new file mode 100644 index 0000000..068efb1 --- /dev/null +++ b/WindowsDevicePortalWrapper/HoloLens/HolographicThermal.cs @@ -0,0 +1,109 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Thermal State enumeration + /// + public enum ThermalStages + { + /// + /// No thermal stage + /// + Normal, + + /// + /// Warm stage + /// + Warm, + + /// + /// Critical stage + /// + Critical, + + /// + /// Unknown stage + /// + Unknown = 9999 + } + + /// + /// Wrappers for Holographic Thermal methods + /// + public partial class DevicePortal + { + /// + /// API for getting the thermal stage + /// + public static readonly string ThermalStageApi = "api/holographic/thermal/stage"; + + /// + /// Gets the current thermal stage reading from the device. + /// + /// ThermalStages enum value. + /// This method is only supported on HoloLens. + public async Task GetThermalStageAsync() + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + ThermalStage thermalStage = await this.GetAsync(ThermalStageApi); + return thermalStage.Stage; + } + + #region Data contract + + /// + /// Object representation of thermal stage + /// + [DataContract] + public class ThermalStage + { + /// + /// Gets the raw stage value + /// + [DataMember(Name = "CurrentStage")] + public int StageRaw { get; private set; } + + /// + /// Gets the enumeration value of the thermal stage + /// + public ThermalStages Stage + { + get + { + ThermalStages stage = ThermalStages.Unknown; + + try + { + stage = (ThermalStages)Enum.ToObject(typeof(ThermalStages), this.StageRaw); + + if (!Enum.IsDefined(typeof(ThermalStages), stage)) + { + stage = ThermalStages.Unknown; + } + } + catch + { + stage = ThermalStages.Unknown; + } + + return stage; + } + } + } + + #endregion // Data contract + } +} diff --git a/WindowsDevicePortalWrapper/HoloLens/MixedRealityCapture.cs b/WindowsDevicePortalWrapper/HoloLens/MixedRealityCapture.cs new file mode 100644 index 0000000..848f145 --- /dev/null +++ b/WindowsDevicePortalWrapper/HoloLens/MixedRealityCapture.cs @@ -0,0 +1,723 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for Mixed reality capture methods + /// + public partial class DevicePortal + { + /// + /// API for getting or deleting a Mixed Reality Capture file. + /// + public static readonly string MrcFileApi = "api/holographic/mrc/file"; + + /// + /// API for getting the list of Holographic Mixed Reality Capture files. + /// + public static readonly string MrcFileListApi = "api/holographic/mrc/files"; + + /// + /// API for taking a Mixed Reality Capture photo. + /// + public static readonly string MrcPhotoApi = "api/holographic/mrc/photo"; + + /// + /// API for getting or setting the default Mixed Reality Capture settings. + /// + public static readonly string MrcSettingsApi = "api/holographic/mrc/settings"; + + /// + /// API for starting a Holographic Mixed Reality Capture recording. + /// + public static readonly string MrcStartRecordingApi = "api/holographic/mrc/video/control/start"; + + /// + /// API for getting the Holographic Mixed Reality Capture status. + /// + public static readonly string MrcStatusApi = "api/holographic/mrc/status"; + + /// + /// API for stopping a Holographic Mixed Reality Capture recording. + /// + public static readonly string MrcStopRecordingApi = "api/holographic/mrc/video/control/stop"; + + /// + /// API for getting a live Holographic Mixed Reality Capture stream. + /// + public static readonly string MrcLiveStreamApi = "api/holographic/stream/live.mp4"; + + /// + /// API for getting a high resolution live Holographic Mixed Reality Capture stream. + /// + public static readonly string MrcLiveStreamHighResApi = "api/holographic/stream/live_high.mp4"; + + /// + /// API for getting a low resolution live Holographic Mixed Reality Capture stream. + /// + public static readonly string MrcLiveStreamLowResApi = "api/holographic/stream/live_low.mp4"; + + /// + /// API for getting a medium resolution live Holographic Mixed Reality Capture stream. + /// + public static readonly string MrcLiveStreamMediumResApi = "api/holographic/stream/live_med.mp4"; + + /// + /// API for getting a mixed reality capture thumbnail + /// + public static readonly string MrcThumbnailApi = "api/holographic/mrc/thumbnail"; + + /// + /// Removes a Mixed Reality Capture file from the device's local storage. + /// + /// The name of the file to be deleted. + /// Task tracking completion of the REST call. + /// This method is only supported on HoloLens. + public async Task DeleteMrcFileAsync(string fileName) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + await this.DeleteAsync( + MrcFileApi, + string.Format("filename={0}", Utilities.Hex64Encode(fileName))); + } + + /// + /// Retrieve the Uri for the high resolution Mixed Reality Capture live stream. + /// + /// Specifies whether or not to include holograms + /// Specifies whether or not to include the color camera + /// Specifies whether or not to include microphone data + /// Specifies whether or not to include audio data + /// Uri used to retreive the Mixed Reality Capture stream. + /// This method is only supported on HoloLens. + public Uri GetHighResolutionMrcLiveStreamUri( + bool includeHolograms = true, + bool includeColorCamera = true, + bool includeMicrophone = true, + bool includeAudio = true) + { + string payload = string.Format( + "holo={0}&pv={1}&mic={2}&loopback={3}", + includeHolograms, + includeColorCamera, + includeMicrophone, + includeAudio).ToLower(); + + return Utilities.BuildEndpoint( + this.deviceConnection.Connection, + MrcLiveStreamHighResApi, + payload); + } + + /// + /// Retrieve the Uri for the low resolution Mixed Reality Capture live stream. + /// + /// Specifies whether or not to include holograms + /// Specifies whether or not to include the color camera + /// Specifies whether or not to include microphone data + /// Specifies whether or not to include audio data + /// Uri used to retreive the Mixed Reality Capture stream. + /// This method is only supported on HoloLens. + public Uri GetLowResolutionMrcLiveStreamUri( + bool includeHolograms = true, + bool includeColorCamera = true, + bool includeMicrophone = true, + bool includeAudio = true) + { + string payload = string.Format( + "holo={0}&pv={1}&mic={2}&loopback={3}", + includeHolograms, + includeColorCamera, + includeMicrophone, + includeAudio).ToLower(); + + return Utilities.BuildEndpoint( + this.deviceConnection.Connection, + MrcLiveStreamLowResApi, + payload); + } + + /// + /// Retrieve the Uri for the medium resolution Mixed Reality Capture live stream. + /// + /// Specifies whether or not to include holograms + /// Specifies whether or not to include the color camera + /// Specifies whether or not to include microphone data + /// Specifies whether or not to include audio data + /// Uri used to retreive the Mixed Reality Capture stream. + /// This method is only supported on HoloLens. + public Uri GetMediumResolutionMrcLiveStreamUri( + bool includeHolograms = true, + bool includeColorCamera = true, + bool includeMicrophone = true, + bool includeAudio = true) + { + string payload = string.Format( + "holo={0}&pv={1}&mic={2}&loopback={3}", + includeHolograms, + includeColorCamera, + includeMicrophone, + includeAudio).ToLower(); + + return Utilities.BuildEndpoint( + this.deviceConnection.Connection, + MrcLiveStreamMediumResApi, + payload); + } + + /// + /// Gets the capture file data + /// + /// Name of the file to retrieve. + /// Specifies whether or not we are requesting a thumbnail image. + /// Byte array containing the file data. + /// This method is only supported on HoloLens. + public async Task GetMrcFileDataAsync( + string fileName, + bool isThumbnailRequest = false) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + byte[] dataBytes = null; + + string apiPath = isThumbnailRequest ? MrcThumbnailApi : MrcFileApi; + + string payload = string.Format("filename={0}", Utilities.Hex64Encode(fileName)); + Uri uri = Utilities.BuildEndpoint( + this.deviceConnection.Connection, + apiPath, + payload); + + using (Stream dataStream = await this.GetAsync(uri)) + { + dataBytes = new byte[dataStream.Length]; + dataStream.Read(dataBytes, 0, dataBytes.Length); + } + + return dataBytes; + } + + /// + /// Gets the list of capture files + /// + /// List of the capture files + /// This method is only supported on HoloLens. + public async Task GetMrcFileListAsync() + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + MrcFileList mrcFileList = await this.GetAsync(MrcFileListApi); + + foreach (MrcFileInformation mfi in mrcFileList.Files) + { + try + { + mfi.Thumbnail = await this.GetMrcThumbnailDataAsync(mfi.FileName); + } + catch + { + } + } + + return mrcFileList; + } + + /// + /// Retrieve the Uri for the Mixed Reality Capture live stream using the default resolution. + /// + /// Specifies whether or not to include holograms + /// Specifies whether or not to include the color camera + /// Specifies whether or not to include microphone data + /// Specifies whether or not to include audio data + /// Uri used to retreive the Mixed Reality Capture stream. + /// This method is only supported on HoloLens. + public Uri GetMrcLiveStreamUri( + bool includeHolograms = true, + bool includeColorCamera = true, + bool includeMicrophone = true, + bool includeAudio = true) + { + string payload = string.Format( + "holo={0}&pv={1}&mic={2}&loopback={3}", + includeHolograms, + includeColorCamera, + includeMicrophone, + includeAudio).ToLower(); + + return Utilities.BuildEndpoint( + this.deviceConnection.Connection, + MrcLiveStreamApi, + payload); + } + + /// + /// Gets the current Mixed Reality Capture settings + /// + /// MrcSettings object containing the current settings + /// This method is only supported on HoloLens. + public async Task GetMrcSettingsAsync() + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + return await this.GetAsync(MrcSettingsApi); + } + + /// + /// Gets the status of the reality capture + /// + /// Status of the capture + /// This method is only supported on HoloLens. + public async Task GetMrcStatusAsync() + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + return await this.GetAsync(MrcStatusApi); + } + + /// + /// Gets thumbnail data for the capture + /// + /// Name of the capture file + /// Byte array containing the thumbnail image data + /// This method is only supported on HoloLens. + public async Task GetMrcThumbnailDataAsync(string fileName) + { + // GetMrcFileData checks for the appropriate platform. We do not need to duplicate the check here. + return await this.GetMrcFileDataAsync(fileName, true); + } + + /// + /// Sets the default Mixed Reality Capture settings + /// + /// Mixed Reality Capture settings to be used as the default. + /// Task tracking completion of the REST call. + /// This method is only supported on HoloLens. + public async Task SetMrcSettingsAsync(MrcSettings settings) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + string payload = string.Format( + "holo={0}&pv={1}&mic={2}&appAudio={3}&vstabbuffer={4}", + settings.IncludeHolograms.ToString().ToLower(), + settings.IncludeColorCamera.ToString().ToLower(), + settings.IncludeMicrophone.ToString().ToLower(), + settings.IncludeAudio.ToString().ToLower(), + settings.VideoStabilizationBuffer); + + await this.PostAsync( + MrcSettingsApi, + payload); + } + + /// + /// Starts a Mixed Reality Capture recording. + /// + /// Specifies whether or not to include holograms + /// Specifies whether or not to include the color camera + /// Specifies whether or not to include microphone data + /// Specifies whether or not to include audio data + /// Task tracking completion of the REST call. + /// This method is only supported on HoloLens. + public async Task StartMrcRecordingAsync( + bool includeHolograms = true, + bool includeColorCamera = true, + bool includeMicrophone = true, + bool includeAudio = true) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + string payload = string.Format( + "holo={0}&pv={1}&mic={2}&loopback={3}", + includeHolograms, + includeColorCamera, + includeMicrophone, + includeAudio).ToLower(); + + await this.PostAsync( + MrcStartRecordingApi, + payload); + } + + /// + /// Stops the Mixed Reality Capture recording + /// + /// Task tracking completion of the REST call. + /// This method is only supported on HoloLens. + public async Task StopMrcRecordingAsync() + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + await this.PostAsync(MrcStopRecordingApi); + } + + /// + /// Take a Mixed Reality Capture photo + /// + /// Specifies whether or not to include holograms + /// Specifies whether or not to include the color camera + /// Task tracking completion of the REST call. + /// This method is only supported on HoloLens. + public async Task TakeMrcPhotoAsync( + bool includeHolograms = true, + bool includeColorCamera = true) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + await this.PostAsync( + MrcPhotoApi, + string.Format("holo={0}&pv={1}", includeHolograms, includeColorCamera).ToLower()); + } + + #region Data contract + /// + /// Object representation of the capture file list + /// + [DataContract] + public class MrcFileList + { + /// + /// Gets the list of files + /// + [DataMember(Name = "MrcRecordings")] + public List Files { get; private set; } + } + + /// + /// Object representation of an individual capture file + /// + [DataContract] + public class MrcFileInformation + { + /// + /// Gets the raw creation time + /// + [DataMember(Name = "CreationTime")] + public long CreationTimeRaw { get; private set; } + + /// + /// Gets the filename + /// + [DataMember(Name = "FileName")] + public string FileName { get; private set; } + + /// + /// Gets the file size + /// + [DataMember(Name = "FileSize")] + public uint FileSize { get; private set; } + + /// + /// Gets the thumbnail + /// + public byte[] Thumbnail { get; internal set; } + + /// + /// Gets the creation time + /// + public DateTime Created + { + get { return new DateTime(this.CreationTimeRaw); } + } + } + + /// + /// Object representation of the Capture status + /// + [DataContract] + public class MrcStatus + { + /// + /// Gets a value indicating whether the device is recording + /// + [DataMember(Name = "IsRecording")] + public bool IsRecording { get; private set; } + + /// + /// Gets the recording status + /// + [DataMember(Name = "ProcessStatus")] + public MrcProcessStatus Status { get; private set; } + } + + /// + /// Object representation of the Mixed Reality Capture process status + /// + [DataContract] + public class MrcProcessStatus + { + /// + /// Gets the raw data for the Mixed Reality Capture process status + /// + [DataMember(Name = "MrcProcess")] + public string MrcProcessRaw { get; private set; } + + /// + /// Gets the status of the Mixed Reality Capture process + /// + public ProcessStatus MrcProcess + { + get + { + return (this.MrcProcessRaw == "Running") ? ProcessStatus.Running : ProcessStatus.Stopped; + } + } + } + + /// + /// Object representation of a Mixed Reality Capture setting. + /// + [DataContract] + public class MrcSetting + { + /// + /// Gets or sets the name of the setting + /// + [DataMember(Name = "Setting")] + public string Setting { get; set; } + + /// + /// Gets or sets the value of the setting + /// + [DataMember(Name = "Value")] + public object Value { get; set; } + } + + /// + /// Object representing the collection of Mixed Reality Capture settings + /// + [DataContract] + public class MrcSettings + { + /// + /// Gets the collection of settings + /// + [DataMember(Name = "MrcSettings")] + public List Settings { get; private set; } + + /// + /// Gets or sets a value indicating whether or not holograms are included. + /// + public bool IncludeHolograms + { + get + { + object setting = this.GetSetting("EnableHolograms"); + + if (setting == null) + { + return true; + } + + return (bool)setting; + } + + set + { + this.SetSetting( + "EnableHolograms", + value); + } + } + + /// + /// Gets or sets a value indicating whether or not color camera data is included. + /// + public bool IncludeColorCamera + { + get + { + object setting = this.GetSetting("EnableCamera"); + + if (setting == null) + { + return true; + } + + return (bool)setting; + } + + set + { + this.SetSetting( + "EnableCamera", + value); + } + } + + /// + /// Gets or sets a value indicating whether or not microphone audio is included. + /// + public bool IncludeMicrophone + { + get + { + object setting = this.GetSetting("EnableMicrophone"); + + if (setting == null) + { + return true; + } + + return (bool)setting; + } + + set + { + this.SetSetting( + "EnableMicrophone", + value); + } + } + + /// + /// Gets or sets a value indicating whether or not audio is included. + /// + public bool IncludeAudio + { + get + { + object setting = this.GetSetting("EnableSystemAudio"); + + if (setting == null) + { + return true; + } + + return (bool)setting; + } + + set + { + this.SetSetting( + "EnableSystemAudio", + value); + } + } + + /// + /// Gets or sets the size, in frames, of the video stabilization buffer. + /// + public int VideoStabilizationBuffer + { + get + { + object setting = this.GetSetting("VideoStabilizationBuffer"); + + if (setting == null) + { + return 0; + } + + return (int)setting; + } + + set + { + if (value < 0) + { + throw new ArgumentException("The video stabilization buffer value must be >= 0"); + } + + this.SetSetting( + "VideoStabilizationBuffer", + value); + } + } + + /// + /// Gets the value of a setting + /// + /// The name of the setting + /// The value of the setting, or if not found, null. + private object GetSetting(string settingName) + { + object value = null; + + foreach (MrcSetting setting in this.Settings) + { + if (setting.Setting == settingName) + { + value = setting.Value; + break; + } + } + + return value; + } + + /// + /// Sets the value of a Mixed Reality Capture setting. + /// + /// The name of the setting + /// The value of the setting + private void SetSetting( + string settingName, + object value) + { + // If the setting exists, update the value, otherwise create a new one. + MrcSetting mrcSetting = null; + + foreach (MrcSetting setting in this.Settings) + { + if (setting.Setting == settingName) + { + mrcSetting = setting; + break; + } + } + + if (mrcSetting != null) + { + mrcSetting.Value = value; + } + else + { + mrcSetting = new MrcSetting(); + mrcSetting.Setting = settingName; + mrcSetting.Value = value; + + this.Settings.Add(mrcSetting); + } + } + } + #endregion Data contract + } +} diff --git a/WindowsDevicePortalWrapper/HoloLens/PerceptionSimulationPlayback.cs b/WindowsDevicePortalWrapper/HoloLens/PerceptionSimulationPlayback.cs new file mode 100644 index 0000000..2ce05f0 --- /dev/null +++ b/WindowsDevicePortalWrapper/HoloLens/PerceptionSimulationPlayback.cs @@ -0,0 +1,426 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for Perception Simulation Playback methods + /// + public partial class DevicePortal + { + /// + /// API for loading or unloading a Holographic Perception Simulation recording. + /// + public static readonly string HolographicSimulationPlaybackSessionFileApi = "api/holographic/simulation/playback/session/file"; + + /// + /// API for pausing a Holographic Perception Simulation recording. + /// + public static readonly string HolographicSimulationPlaybackPauseApi = "api/holographic/simulation/playback/session/pause"; + + /// + /// API for uploading or deleting a Holographic Perception Simulation recording file. + /// + public static readonly string HolographicSimulationPlaybackFileApi = "api/holographic/simulation/playback/file"; + + /// + /// API for retrieving a list of a Holographic Perception Simulation recording files. + /// + public static readonly string HolographicSimulationPlaybackFilesApi = "api/holographic/simulation/playback/files"; + + /// + /// API for starting playback of a Holographic Perception Simulation recording. + /// + public static readonly string HolographicSimulationPlaybackPlayApi = "api/holographic/simulation/playback/session/play"; + + /// + /// API for getting the list of loaded Holographic Perception Simulation files. + /// + public static readonly string HolographicSimulationPlaybackSessionFilesApi = "api/holographic/simulation/playback/session/files"; + + /// + /// API for retrieving the playback state of a Holographic Perception Simulation recording. + /// + public static readonly string HolographicSimulationPlaybackStateApi = "api/holographic/simulation/playback/session"; + + /// + /// API for starting playback of a Holographic Perception Simulation recording. + /// + public static readonly string HolographicSimulationPlaybackStopApi = "api/holographic/simulation/playback/session/stop"; + + /// + /// API for retrieving the types of data in a Holographic Perception Simulation recording. + /// + public static readonly string HolographicSimulationPlaybackDataTypesApi = "api/holographic/simulation/playback/session/types"; + + /// + /// Enumeration describing the available Holgraphic Simulation playback states. + /// + public enum HolographicSimulationPlaybackStates + { + /// + /// The simulation has been stopped. + /// + Stopped = 0, + + /// + /// The simulation is playing. + /// + Playing, + + /// + /// The simulation has been paused. + /// + Paused, + + /// + /// Playback has completed. + /// + Complete, + + /// + /// Playback is in an unexpected / unknown state. + /// + Unexpected = 9999 + } + + /// + /// Deletes the specified Holographic Simulation recording. + /// + /// The name of the recording to delete (ex: testsession.xef). + /// Task tracking completion of the REST call. + /// This method is only supported on HoloLens. + public async Task DeleteHolographicSimulationRecordingAsync(string name) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + string payload = string.Format( + "recording={0}", + name); + + await this.DeleteAsync(HolographicSimulationPlaybackFileApi, payload); + } + + /// + /// Gets the collection of Holographic Perception Simulation files on this HoloLens. + /// + /// HolographicSimulationPlaybackFiles object representing the files on the HoloLens + /// This method is only supported on HoloLens. + public async Task GetHolographicSimulationPlaybackFilesAsync() + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + return await this.GetHolographicSimulationPlaybackFilesPrivateAsync(false); + } + + /// + /// Gets the collection of loaded Holographic Perception Simulation files on this HoloLens. + /// + /// HolographicSimulationPlaybackFiles object representing the files loaded on the HoloLens + /// This method is only supported on HoloLens. + public async Task GetHolographicSimulationPlaybackSessionFilesAsync() + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + return await this.GetHolographicSimulationPlaybackFilesPrivateAsync(true); + } + + /// + /// Gets the types of data that are in a loaded Holographic Perception Simulation file. + /// + /// Name of the recording file, with extension. + /// HolographicSimulationDataTypes object representing they types of data in the file + /// This method is only supported on HoloLens. + public async Task GetHolographicSimulationPlaybackSessionDataTypesAsync(string recordingName) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + string payload = string.Format( + "recording={0}", + recordingName); + + return await this.GetAsync( + HolographicSimulationPlaybackDataTypesApi, + payload); + } + + /// + /// Gets the playback state of a Holographic Simulation recording. + /// + /// The name of the recording (ex: testsession.xef). + /// HolographicSimulationPlaybackStates enum value describing the state of the recording. + /// This method is only supported on HoloLens. + public async Task GetHolographicSimulationPlaybackStateAsync(string name) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + HolographicSimulationPlaybackStates playbackState = HolographicSimulationPlaybackStates.Unexpected; + + string payload = string.Format( + "recording={0}", + name); + + Uri uri = Utilities.BuildEndpoint( + this.deviceConnection.Connection, + HolographicSimulationPlaybackStateApi, + payload); + + using (Stream dataStream = await this.GetAsync(uri)) + { + if ((dataStream != null) && + (dataStream.Length != 0)) + { + // Try to get the session state. + try + { + DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(HolographicSimulationPlaybackSessionState)); + HolographicSimulationPlaybackSessionState sessionState = (HolographicSimulationPlaybackSessionState)serializer.ReadObject(dataStream); + playbackState = sessionState.State; + } + catch + { + // We did not receive the session state, check to see if we received a simulation error. + dataStream.Position = 0; + DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(HolographicSimulationError)); + HolographicSimulationError error = (HolographicSimulationError)serializer.ReadObject(dataStream); + throw new InvalidOperationException(error.Reason); + } + } + } + + return playbackState; + } + + /// + /// Loads the specified Holographic Simulation recording. + /// + /// The name of the recording to load (ex: testsession.xef). + /// Task tracking completion of the REST call. + /// This method is only supported on HoloLens. + public async Task LoadHolographicSimulationRecordingAsync(string recordingName) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + string payload = string.Format( + "recording={0}", + recordingName); + + await this.PostAsync(HolographicSimulationPlaybackSessionFileApi, payload); + } + + /// + /// Pauses playback of a Holographic Perception Simulation recording + /// + /// The name of the recording to pause + /// Task tracking completion of the REST call. + /// This method is only supported on HoloLens. + public async Task PauseHolographicSimulationRecordingAsync(string recordingName) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + string payload = string.Format( + "recording={0}", + recordingName); + + await this.PostAsync(HolographicSimulationPlaybackPauseApi, payload); + } + + /// + /// Starts playback of a Holographic Perception Simulation recording + /// + /// The name of the recording to play + /// Task tracking completion of the REST call. + /// This method is only supported on HoloLens. + public async Task PlayHolographicSimulationRecordingAsync(string recordingName) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + string payload = string.Format( + "recording={0}", + recordingName); + + await this.PostAsync(HolographicSimulationPlaybackPlayApi, payload); + } + + /// + /// Stops playback of a Holographic Perception Simulation recording + /// + /// The name of the recording to stop + /// Task tracking completion of the REST call. + /// This method is only supported on HoloLens. + public async Task StopHolographicSimulationRecordingAsync(string recordingName) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + string payload = string.Format( + "recording={0}", + recordingName); + + await this.PostAsync(HolographicSimulationPlaybackStopApi, payload); + } + + /// + /// Unloads the specified Holographic Simulation recording. + /// + /// The name of the recording to unload (ex: testsession.xef). + /// Task tracking completion of the REST call. + /// This method is only supported on HoloLens. + public async Task UnloadHolographicSimulationRecordingAsync(string recordingName) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + string payload = string.Format( + "recording={0}", + recordingName); + + await this.DeleteAsync(HolographicSimulationPlaybackSessionFileApi, payload); + } + + /// + /// Gets the collection of Holographic Perception Simulation files + /// + /// Value indicating whether or not to return loaded files. + /// Collection of Holographic Perception simulation file names + private async Task GetHolographicSimulationPlaybackFilesPrivateAsync(bool session) + { + string apiPath = session ? HolographicSimulationPlaybackSessionFilesApi : HolographicSimulationPlaybackFilesApi; + + return await this.GetAsync(apiPath); + } + + #region Data contract + /// + /// Object representing the data types in a Holographic Perception Simulation file + /// + [DataContract] + public class HolographicSimulationDataTypes + { + /// + /// Gets a value indicating whether or not the file contains hand data. + /// + [DataMember(Name = "hands")] + public bool IncludesHands { get; private set; } + + /// + /// Gets a value indicating whether or not the file contains head data. + /// + [DataMember(Name = "head")] + public bool IncludesHead { get; private set; } + + /// + /// Gets a value indicating whether or not the file contains environmentatl data. + /// + [DataMember(Name = "environment")] + public bool IncludesEnvironment { get; private set; } + + /// + /// Gets a value indicating whether or not the file contains spatial mapping data. + /// + [DataMember(Name = "spatialMapping")] + public bool IncludesSpatialMapping { get; private set; } + } + + /// + /// Object representation of the Holographic Perception Simulation files collection + /// + [DataContract] + public class HolographicSimulationPlaybackFiles + { + /// + /// Gets the list of recording file names. + /// + [DataMember(Name = "recordings")] + public List Files { get; private set; } + } + + /// + /// Object representation of the Holographic Perception Simulation playback state + /// + [DataContract] + public class HolographicSimulationPlaybackSessionState + { + /// + /// Gets the state value as a string + /// + [DataMember(Name = "state")] + public string StateRaw { get; private set; } + + /// + /// Gets the playback session state + /// + public HolographicSimulationPlaybackStates State + { + get + { + HolographicSimulationPlaybackStates state = HolographicSimulationPlaybackStates.Unexpected; + + switch (this.StateRaw) + { + case "stopped": + state = HolographicSimulationPlaybackStates.Stopped; + break; + + case "playing": + state = HolographicSimulationPlaybackStates.Playing; + break; + + case "paused": + state = HolographicSimulationPlaybackStates.Paused; + break; + + case "end": + state = HolographicSimulationPlaybackStates.Complete; + break; + + default: + state = HolographicSimulationPlaybackStates.Unexpected; + break; + } + + return state; + } + } + } + #endregion // Data contract + } +} diff --git a/WindowsDevicePortalWrapper/HoloLens/PerceptionSimulationRecording.cs b/WindowsDevicePortalWrapper/HoloLens/PerceptionSimulationRecording.cs new file mode 100644 index 0000000..a97d11e --- /dev/null +++ b/WindowsDevicePortalWrapper/HoloLens/PerceptionSimulationRecording.cs @@ -0,0 +1,162 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.IO; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Wrappers for Perception Simulation Recording methods + /// + public partial class DevicePortal + { + /// + /// API for getting a Holographic Perception Simulation recording status. + /// + public static readonly string HolographicSimulationRecordingStatusApi = "api/holographic/simulation/recording/status"; + + /// + /// API for starting a Holographic Perception Simulation recording. + /// + public static readonly string StartHolographicSimulationRecordingApi = "api/holographic/simulation/recording/start"; + + /// + /// API for stopping a Holographic Perception Simulation recording. + /// + public static readonly string StopHolographicSimulationRecordingApi = "api/holographic/simulation/recording/stop"; + + /// + /// Gets the holographic simulation recording status. + /// + /// True if recording, false otherwise. + /// This method is only supported on HoloLens. + public async Task GetHolographicSimulationRecordingStatusAsync() + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + HolographicSimulationRecordingStatus status = await this.GetAsync(HolographicSimulationRecordingStatusApi); + return status.IsRecording; + } + + /// + /// Starts a Holographic Simulation recording session. + /// + /// The name of the recording. + /// Should head data be recorded? The default value is true. + /// Should hand data be recorded? The default value is true. + /// Should Spatial Mapping data be recorded? The default value is true. + /// Should environment data be recorded? The default value is true. + /// Task tracking completion of the REST call. + /// This method is only supported on HoloLens. + public async Task StartHolographicSimulationRecordingAsync( + string name, + bool recordHead = true, + bool recordHands = true, + bool recordSpatialMapping = true, + bool recordEnvironment = true) + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + string payload = string.Format( + "head={0}&hands={1}&spatialMapping={2}&environment={3}&name={4}", + recordHead ? 1 : 0, + recordHands ? 1 : 0, + recordSpatialMapping ? 1 : 0, + recordEnvironment ? 1 : 0, + name); + await this.PostAsync(StartHolographicSimulationRecordingApi, payload); + } + + /// + /// Stops a Holographic Simulation recording session. + /// + /// Byte array containing the recorded data. + /// No recording was in progress. + /// This method is only supported on HoloLens. + public async Task StopHolographicSimulationRecordingAsync() + { + if (!Utilities.IsHoloLens(this.Platform, this.DeviceFamily)) + { + throw new NotSupportedException("This method is only supported on HoloLens."); + } + + Uri uri = Utilities.BuildEndpoint( + this.deviceConnection.Connection, + StopHolographicSimulationRecordingApi); + + byte[] dataBytes = null; + + using (Stream dataStream = await this.GetAsync(uri)) + { + if ((dataStream != null) && + (dataStream.Length != 0)) + { + DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(HolographicSimulationError)); + HolographicSimulationError error = null; + + try + { + // Try to get / interpret an error response. + error = (HolographicSimulationError)serializer.ReadObject(dataStream); + } + catch + { + } + + if (error != null) + { + // We received an error response. + throw new InvalidOperationException(error.Reason); + } + + // Getting here indicates that we have file data to return. + dataBytes = new byte[dataStream.Length]; + dataStream.Read(dataBytes, 0, dataBytes.Length); + } + } + + return dataBytes; + } + + #region Data contract + /// + /// Object representation of a Holographic Simulation (playback or recording) error. + /// + [DataContract] + public class HolographicSimulationError + { + /// + /// Gets the Reason string. + /// + [DataMember(Name = "Reason")] + public string Reason { get; private set; } + } + + /// + /// Object representation of Holographic Simulation recording status. + /// + [DataContract] + public class HolographicSimulationRecordingStatus + { + /// + /// Gets a value indicating whether the simulation is recording. + /// + [DataMember(Name = "recording")] + public bool IsRecording { get; private set; } + } + #endregion // Data contract + } +} diff --git a/WindowsDevicePortalWrapper/HttpMultipartFileContent.cs b/WindowsDevicePortalWrapper/HttpMultipartFileContent.cs new file mode 100644 index 0000000..f036ca1 --- /dev/null +++ b/WindowsDevicePortalWrapper/HttpMultipartFileContent.cs @@ -0,0 +1,141 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// This class mimicks , with two main differences + /// 1. Simplifies posting files by taking file names instead of managing streams. + /// 2. Does not quote the boundaries, due to a bug in the device portal + /// + internal sealed class HttpMultipartFileContent : HttpContent + { + /// + /// List of items to transfer + /// + private List items = new List(); + + /// + /// Boundary string + /// + private string boundaryString; + + /// + /// Initializes a new instance of the class. + /// + public HttpMultipartFileContent() : this(Guid.NewGuid().ToString()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The boundary string for file content. + public HttpMultipartFileContent(string boundary) + { + this.boundaryString = boundary; + Headers.TryAddWithoutValidation("Content-Type", string.Format("multipart/form-data; boundary={0}", this.boundaryString)); + } + + /// + /// Adds a file to the list of items to transfer + /// + /// The name of the file to add + public void Add(string filename) + { + if (filename != null) + { + this.items.Add(filename); + } + } + + /// + /// Adds a range of files to the list of items to transfer + /// + /// List of files to add + public void AddRange(IEnumerable filenames) + { + if (filenames != null) + { + this.items.AddRange(filenames); + } + } + + /// + /// Serializes the stream. + /// + /// Serialized Stream + /// The Transport Context + /// Task tracking progress + protected override async Task SerializeToStreamAsync(Stream outStream, TransportContext context) + { + var boundary = Encoding.ASCII.GetBytes($"--{boundaryString}\r\n"); + var newline = Encoding.ASCII.GetBytes("\r\n"); + foreach (var item in this.items) + { + outStream.Write(boundary, 0, boundary.Length); + var headerdata = GetFileHeader(new FileInfo(item)); + outStream.Write(headerdata, 0, headerdata.Length); + + using (var file = File.OpenRead(item)) + { + await file.CopyToAsync(outStream); + } + + outStream.Write(newline, 0, newline.Length); + await outStream.FlushAsync(); + } + + // Close the installation request data. + boundary = Encoding.ASCII.GetBytes($"--{boundaryString}--\r\n"); + outStream.Write(boundary, 0, boundary.Length); + await outStream.FlushAsync(); + } + + /// + /// Computes required length for the transfer. + /// + /// The computed length value + /// Whether or not the length was successfully computed + protected override bool TryComputeLength(out long length) + { + length = 0; + var boundaryLength = Encoding.ASCII.GetBytes(string.Format("--{0}\r\n", this.boundaryString)).Length; + foreach (var item in this.items) + { + var headerdata = GetFileHeader(new FileInfo(item)); + length += boundaryLength + headerdata.Length + new FileInfo(item).Length + 2; + } + + length += boundaryLength + 2; + return true; + } + + /// + /// Gets the file header for the transfer + /// + /// Information about the file + /// A byte array with the file header information + private static byte[] GetFileHeader(FileInfo info) + { + string contentType = "application/octet-stream"; + if (info.Extension.ToLower() == ".cer") + { + contentType = "application/x-x509-ca-cert"; + } + + return Encoding.ASCII.GetBytes(string.Format("Content-Disposition: form-data; name=\"{0}\"; filename=\"{0}\"\r\nContent-Type: {1}\r\n\r\n", info.Name, contentType)); + } + } +} \ No newline at end of file diff --git a/WindowsDevicePortalWrapper/HttpRest/HttpHeadersHelper.cs b/WindowsDevicePortalWrapper/HttpRest/HttpHeadersHelper.cs new file mode 100644 index 0000000..11d9527 --- /dev/null +++ b/WindowsDevicePortalWrapper/HttpRest/HttpHeadersHelper.cs @@ -0,0 +1,152 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +#if !WINDOWS_UWP +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection; +using System.Threading.Tasks; +#else +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Windows.Web.Http; +using Windows.Web.Http.Headers; +#endif // !WINDOWS_UWP + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Methods for working with Http headers. + /// + public partial class DevicePortal + { + /// + /// Header name for Content Type of a request body. + /// + private static readonly string ContentTypeHeaderName = "Content-Type"; + + /// + /// Header name for a CSRF-Token. + /// + private static readonly string CsrfTokenName = "CSRF-Token"; + + /// + /// Header name for a User-Agent. + /// + private static readonly string UserAgentName = "User-Agent"; + + /// + /// Header value for User-Agent for the WDPW Open Source project. + /// + private static readonly string UserAgentValue = "WindowsDevicePortalWrapper"; + + /// + /// CSRF token retrieved by GET calls and used on subsequent POST/DELETE/PUT calls. + /// This token is intended to prevent a security vulnerability from cross site forgery. + /// + private string csrfToken = string.Empty; + + /// + /// Applies the CSRF token to the HTTP client. + /// + /// The HTTP client on which to have the header set. + /// The HTTP method (ex: POST) that will be called on the client. + private void ApplyCSRFHeader( + HttpClient client, + HttpMethods method) + { + string headerName = "X-" + CsrfTokenName; + string headerValue = this.csrfToken; + + if (string.Compare(method.ToString(), "get", true) == 0) + { + headerName = CsrfTokenName; + headerValue = string.IsNullOrEmpty(this.csrfToken) ? "Fetch" : headerValue; + } + +#if WINDOWS_UWP + HttpRequestHeaderCollection headers = client.DefaultRequestHeaders; +#else + HttpRequestHeaders headers = client.DefaultRequestHeaders; +#endif // WINDOWS_UWP + + headers.Add(headerName, headerValue); + } + + /// + /// Applies any needed headers to the HTTP client. + /// + /// The HTTP client on which to have the headers set. + /// The HTTP method (ex: POST) that will be called on the client. + private void ApplyHttpHeaders( + HttpClient client, + HttpMethods method) + { + this.ApplyUserAgentHeader(client); + this.ApplyCSRFHeader(client, method); + } + + /// + /// Adds the User-Agent string to a request to identify it + /// as coming from the WDPW Open Source project. + /// + /// The HTTP client on which to have the header set. + private void ApplyUserAgentHeader(HttpClient client) + { + string userAgentValue = UserAgentValue; + +#if WINDOWS_UWP + Assembly asm = this.GetType().GetTypeInfo().Assembly; + userAgentValue += "-v" + asm.GetName().Version.ToString(); + userAgentValue += "-UWP"; + HttpRequestHeaderCollection headers = client.DefaultRequestHeaders; +#else + Assembly asm = Assembly.GetExecutingAssembly(); + userAgentValue += "-v" + asm.GetName().Version.ToString(); + userAgentValue += "-dotnet"; + HttpRequestHeaders headers = client.DefaultRequestHeaders; +#endif // WINDOWS_UWP + + headers.Add(UserAgentName, userAgentValue); + } + + /// + /// Retrieves the CSRF token from the HTTP response and stores it. + /// + /// The HTTP response from which to retrieve the header. + private void RetrieveCsrfToken(HttpResponseMessage response) + { + // If the response sets a CSRF token, store that for future requests. +#if WINDOWS_UWP + string cookie; + if (response.Headers.TryGetValue("Set-Cookie", out cookie)) + { + string csrfTokenNameWithEquals = CsrfTokenName + "="; + if (cookie.StartsWith(csrfTokenNameWithEquals)) + { + this.csrfToken = cookie.Substring(csrfTokenNameWithEquals.Length); + } + } +#else + IEnumerable cookies; + if (response.Headers.TryGetValues("Set-Cookie", out cookies)) + { + foreach (string cookie in cookies) + { + string csrfTokenNameWithEquals = CsrfTokenName + "="; + if (cookie.StartsWith(csrfTokenNameWithEquals)) + { + this.csrfToken = cookie.Substring(csrfTokenNameWithEquals.Length); + } + } + } +#endif + } + } +} diff --git a/WindowsDevicePortalWrapper/HttpRest/RequestHelpers.cs b/WindowsDevicePortalWrapper/HttpRest/RequestHelpers.cs new file mode 100644 index 0000000..14c4415 --- /dev/null +++ b/WindowsDevicePortalWrapper/HttpRest/RequestHelpers.cs @@ -0,0 +1,44 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System.IO; +using System.Runtime.Serialization; +using System.Text; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Methods for working with Http requests. + /// + public partial class DevicePortal + { + /// + /// Copies a file to the specified stream and prepends the necessary content information + /// required to be part of a multipart form data request. + /// + /// The file to be copied. + /// The stream to which the file will be copied. + private static void CopyFileToRequestStream( + FileInfo file, + Stream stream) + { + byte[] data; + string contentDisposition = string.Format("Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\n", file.Name, file.Name); + string contentType = "Content-Type: application/octet-stream\r\n\r\n"; + + data = Encoding.ASCII.GetBytes(contentDisposition); + stream.Write(data, 0, data.Length); + + data = Encoding.ASCII.GetBytes(contentType); + stream.Write(data, 0, data.Length); + + using (FileStream fs = File.OpenRead(file.FullName)) + { + fs.CopyTo(stream); + } + } + } +} diff --git a/WindowsDevicePortalWrapper/HttpRest/ResponseHelpers.cs b/WindowsDevicePortalWrapper/HttpRest/ResponseHelpers.cs new file mode 100644 index 0000000..165a88b --- /dev/null +++ b/WindowsDevicePortalWrapper/HttpRest/ResponseHelpers.cs @@ -0,0 +1,115 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Text.RegularExpressions; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Methods for working with Http responses. + /// + public partial class DevicePortal + { + /// + /// The prefix for the JSON formatting error. + /// + private static readonly string SysPerfInfoErrorPrefix = "{\"Reason\" : \""; + + /// + /// The postfix for the JSON formatting error. + /// + private static readonly string SysPerfInfoErrorPostfix = "\"}"; + + /// + /// Reads dataStream as T. + /// + /// Return type for the JSON message + /// The stream that contains the JSON message to be checked. + /// Optional settings for JSON serialization. + /// Read data + public static T ReadJsonStream(Stream dataStream, DataContractJsonSerializerSettings settings = null) + { + T data = default(T); + object response = null; + DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T), settings); + + using (dataStream) + { + if ((dataStream != null) && + (dataStream.Length != 0)) + { + JsonFormatCheck(dataStream); + + try + { + response = serializer.ReadObject(dataStream); + } + catch (SerializationException) + { + // Assert on serialization failure. + Debug.Assert(false, "Serialization failure encountered. Check DataContract types for a possible mismatch between expectations and reality"); + throw; + } + + data = (T)response; + } + } + + return data; + } + + /// + /// Checks the JSON for any known formatting errors and fixes them. + /// + /// Return type for the JSON message + /// The stream that contains the JSON message to be checked. + private static void JsonFormatCheck(Stream jsonStream) + { + if (typeof(T) == typeof(SystemPerformanceInformation)) + { + StreamReader read = new StreamReader(jsonStream); + string rawJsonString = read.ReadToEnd(); + + // Recover from an error in which SystemPerformanceInformation is returned with an incorrect prefix, postfix and the message converted into JSON a second time. + if (rawJsonString.StartsWith(SysPerfInfoErrorPrefix, StringComparison.OrdinalIgnoreCase) && rawJsonString.EndsWith(SysPerfInfoErrorPostfix, StringComparison.OrdinalIgnoreCase)) + { + // Remove the incorrect prefix and postfix from the JSON message. + rawJsonString = rawJsonString.Substring(SysPerfInfoErrorPrefix.Length, rawJsonString.Length - SysPerfInfoErrorPrefix.Length - SysPerfInfoErrorPostfix.Length); + + // Undo the second JSON conversion. + rawJsonString = Regex.Replace(rawJsonString, "\\\\\"", "\"", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + rawJsonString = Regex.Replace(rawJsonString, "\\\\\\\\", "\\", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + // Overwrite the stream with the fixed JSON. + jsonStream.SetLength(0); + var sw = new StreamWriter(jsonStream); + sw.Write(rawJsonString); + sw.Flush(); + } + + jsonStream.Seek(0, SeekOrigin.Begin); + } + } + + #region Data contract + + /// + /// A null response class when we don't care about the response + /// body. + /// + [DataContract] + private class NullResponse + { + } + + #endregion + } +} diff --git a/WindowsDevicePortalWrapper/HttpRest/RestDelete.cs b/WindowsDevicePortalWrapper/HttpRest/RestDelete.cs new file mode 100644 index 0000000..71045e7 --- /dev/null +++ b/WindowsDevicePortalWrapper/HttpRest/RestDelete.cs @@ -0,0 +1,53 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.IO; +using System.Runtime.Serialization.Json; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// HTTP DELETE Wrapper + /// + public partial class DevicePortal + { + /// + /// Calls the specified API with the provided payload. This signature leaves + /// off the optional response so callers who don't need a response body + /// don't need to specify a type for it. + /// + /// The relative portion of the uri path that specifies the API to call. + /// The query string portion of the uri path that provides the parameterized data. + /// Task tracking the HTTP completion. + public async Task DeleteAsync( + string apiPath, + string payload = null) + { + await this.DeleteAsync(apiPath, payload); + } + + /// + /// Calls the specified API with the provided payload. + /// + /// The type of the data for the HTTP response body (if present). + /// The relative portion of the uri path that specifies the API to call. + /// The query string portion of the uri path that provides the parameterized data. + /// Task tracking the HTTP completion. + public async Task DeleteAsync( + string apiPath, + string payload = null) where T : new() + { + Uri uri = Utilities.BuildEndpoint( + this.deviceConnection.Connection, + apiPath, + payload); + + return ReadJsonStream(await this.DeleteAsync(uri)); + } + } +} diff --git a/WindowsDevicePortalWrapper/HttpRest/RestGet.cs b/WindowsDevicePortalWrapper/HttpRest/RestGet.cs new file mode 100644 index 0000000..85aeb31 --- /dev/null +++ b/WindowsDevicePortalWrapper/HttpRest/RestGet.cs @@ -0,0 +1,42 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Runtime.Serialization.Json; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// HTTP GET Wrapper + /// + public partial class DevicePortal + { + /// + /// Calls the specified API with the provided payload. + /// + /// Return type for the GET call + /// The relative portion of the uri path that specifies the API to call. + /// The query string portion of the uri path that provides the parameterized data. + /// An object of the specified type containing the data returned by the request. + public async Task GetAsync( + string apiPath, + string payload = null) where T : new() + { + Uri uri = Utilities.BuildEndpoint( + this.deviceConnection.Connection, + apiPath, + payload); + + return ReadJsonStream(await this.GetAsync(uri).ConfigureAwait(false)); + } + } +} diff --git a/WindowsDevicePortalWrapper/HttpRest/RestPost.cs b/WindowsDevicePortalWrapper/HttpRest/RestPost.cs new file mode 100644 index 0000000..75a7ee1 --- /dev/null +++ b/WindowsDevicePortalWrapper/HttpRest/RestPost.cs @@ -0,0 +1,83 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization.Json; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// HTTP POST Wrapper + /// + public partial class DevicePortal + { + /// + /// Calls the specified API with the provided body. This signature leaves + /// off the optional response so callers who don't need a response body + /// don't need to specify a type for it. + /// + /// The relative portion of the uri path that specifies the API to call. + /// List of files that we want to include in the post request. + /// The query string portion of the uri path that provides the parameterized data. + /// Task tracking the POST completion. + public async Task PostAsync( + string apiPath, + List files, + string payload = null) + { + Uri uri = Utilities.BuildEndpoint( + this.deviceConnection.Connection, + apiPath, + payload); + + var content = new HttpMultipartFileContent(); + content.AddRange(files); + await this.PostAsync(uri, content); + } + + /// + /// Calls the specified API with the provided body. This signature leaves + /// off the optional response so callers who don't need a response body + /// don't need to specify a type for it. + /// + /// The relative portion of the uri path that specifies the API to call. + /// The query string portion of the uri path that provides the parameterized data. + /// Task tracking the POST completion. + public async Task PostAsync( + string apiPath, + string payload = null) + { + await this.PostAsync(apiPath, payload); + } + + /// + /// Calls the specified API with the provided payload. + /// + /// The type of the data for the HTTP response body (if present). + /// The relative portion of the uri path that specifies the API to call. + /// The query string portion of the uri path that provides the parameterized data. + /// Optional stream containing data for the request body. + /// The type of that request body data. + /// Task tracking the POST completion. + public async Task PostAsync( + string apiPath, + string payload = null, + Stream requestStream = null, + string requestStreamContentType = null) where T : new() + { + Uri uri = Utilities.BuildEndpoint( + this.deviceConnection.Connection, + apiPath, + payload); + + return ReadJsonStream(await this.PostAsync(uri, requestStream, requestStreamContentType)); + } + } +} diff --git a/WindowsDevicePortalWrapper/HttpRest/RestPut.cs b/WindowsDevicePortalWrapper/HttpRest/RestPut.cs new file mode 100644 index 0000000..b9759cf --- /dev/null +++ b/WindowsDevicePortalWrapper/HttpRest/RestPut.cs @@ -0,0 +1,98 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.IO; +#if !WINDOWS_UWP +using System.Net.Http; +using System.Net.Http.Headers; +#endif // !WINDOWS_UWP +using System.Runtime.Serialization.Json; +using System.Threading.Tasks; +#if WINDOWS_UWP +using Windows.Foundation; +using Windows.Networking; +using Windows.Security.Credentials; +using Windows.Storage.Streams; +using Windows.Web.Http; +using Windows.Web.Http.Filters; +using Windows.Web.Http.Headers; +#endif // WINDOWS_UWP + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// HTTP PUT Wrapper + /// + public partial class DevicePortal + { + /// + /// Calls the specified API with the provided body. This signature leaves + /// off the optional response so callers who don't need a response body + /// don't need to specify a type for it, which also would force them + /// to explicitly declare their bodyData type instead of letting it + /// be implied implicitly. + /// + /// The type of the data for the HTTP request body. + /// The relative portion of the uri path that specifies the API to call. + /// The data to be used for the HTTP request body. + /// The query string portion of the uri path that provides the parameterized data. + /// Task tracking the PUT completion. + public async Task PutAsync( + string apiPath, + K bodyData, + string payload = null) where K : class + { + await this.PutAsync(apiPath, bodyData, payload); + } + + /// + /// Calls the specified API with the provided body. + /// + /// The type of the data for the HTTP response body (if present). + /// The type of the data for the HTTP request body. + /// The relative portion of the uri path that specifies the API to call. + /// The data to be used for the HTTP request body. + /// The query string portion of the uri path that provides the parameterized data. + /// Task tracking the PUT completion, optional response body. + public async Task PutAsync( + string apiPath, + K bodyData = null, + string payload = null) where T : new() + where K : class + { + Uri uri = Utilities.BuildEndpoint( + this.deviceConnection.Connection, + apiPath, + payload); + +#if WINDOWS_UWP + HttpStreamContent streamContent = null; +#else + StreamContent streamContent = null; +#endif // WINDOWS_UWP + + if (bodyData != null) + { + // Serialize the body to a JSON stream + DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(K)); + Stream stream = new MemoryStream(); + serializer.WriteObject(stream, bodyData); + + stream.Seek(0, SeekOrigin.Begin); +#if WINDOWS_UWP + streamContent = new HttpStreamContent(stream.AsInputStream()); + streamContent.Headers.ContentType = new HttpMediaTypeHeaderValue("application/json"); +#else + streamContent = new StreamContent(stream); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); +#endif // WINDOWS_UWP + } + + return ReadJsonStream(await this.PutAsync(uri, streamContent)); + } + } +} diff --git a/WindowsDevicePortalWrapper/HttpRest/WebSocket.cs b/WindowsDevicePortalWrapper/HttpRest/WebSocket.cs new file mode 100644 index 0000000..8328e0c --- /dev/null +++ b/WindowsDevicePortalWrapper/HttpRest/WebSocket.cs @@ -0,0 +1,170 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Internal Web socket message received event handler + /// + /// sender object + /// Web socket message received args + /// Return type for the websocket messages. + internal delegate void WebSocketMessageReceivedEventInternalHandler(WebSocket sender, WebSocketMessageReceivedEventArgs args); + + /// + /// Internal Web socket stream received event handler + /// + /// sender object + /// Web socket message received args + /// Return type for the websocket. + internal delegate void WebSocketStreamReceivedEventInternalHandler(WebSocket sender, WebSocketMessageReceivedEventArgs args); + + /// + /// HTTP Websocket Wrapper + /// + /// Return type for the websocket messages. + internal partial class WebSocket + { + /// + /// The device that the is connected to. + /// + private IDevicePortalConnection deviceConnection; + + /// + /// Indicates whether the web socket should send streams instead of parsed objects + /// + private bool sendStreams = false; + + /// + /// Gets or sets the message received handler. + /// + public event WebSocketMessageReceivedEventInternalHandler WebSocketMessageReceived; + + /// + /// Gets or sets the stream received handler. + /// + public event WebSocketStreamReceivedEventInternalHandler WebSocketStreamReceived; + + /// + /// Gets a value indicating whether the web socket is connected. + /// + public bool IsConnected + { + get; + private set; + } + + /// + /// Gets a value indicating whether the web socket is listening for messages. + /// + public bool IsListeningForMessages + { + get; + private set; + } + + /// + /// Initialize a connection to the websocket. + /// + /// The relative portion of the uri path that specifies the API to call. + /// The query string portion of the uri path that provides the parameterized data. + /// The task of opening the websocket connection. + internal async Task ConnectAsync(string apiPath, string payload = null) + { + if (this.IsConnected) + { + return; + } + + Uri uri = Utilities.BuildEndpoint( + this.deviceConnection.WebSocketConnection, + apiPath, + payload); + await this.ConnectInternalAsync(uri); + } + + /// + /// Closes the connection to the websocket and stop listening for messages. + /// + /// The task of closing the websocket connection. + internal async Task CloseAsync() + { + if (this.IsConnected) + { + if (this.IsListeningForMessages) + { + await this.StopListeningForMessagesInternalAsync(); + } + + await this.CloseInternalAsync(); + } + } + + /// + /// Starts listening for messages from the websocket. Once they are received they are parsed and the WebSocketMessageReceived event is raised. + /// + /// The task of listening for messages from the websocket. + internal async Task ReceiveMessagesAsync() + { + if (this.IsConnected && !this.IsListeningForMessages) + { + await this.StartListeningForMessagesInternalAsync(); + } + } + + /// + /// Sends a message to the server. + /// + /// The message to send. + /// The task of sending a message to the websocket. + internal async Task SendMessageAsync(string message) + { + if (this.IsConnected) + { + await this.SendMessageInternalAsync(message); + } + } + + /// + /// Converts received stream to a parsed object and passes it to + /// the WebSocketMessageReceived handler. The sendstreams property can be used to + /// override this and send the instead via the WebSocketStreamReceived handler. + /// + /// The received stream. + private void ConvertStreamToMessage(Stream stream) + { + if (stream != null && stream.Length != 0) + { + if (this.sendStreams) + { + this.WebSocketStreamReceived?.Invoke( + this, + new WebSocketMessageReceivedEventArgs(stream)); + } + else + { + DataContractJsonSerializerSettings settings = new DataContractJsonSerializerSettings() + { + UseSimpleDictionaryFormat = true + }; + + T message = DevicePortal.ReadJsonStream(stream, settings); + + this.WebSocketMessageReceived?.Invoke( + this, + new WebSocketMessageReceivedEventArgs(message)); + } + } + } + } +} \ No newline at end of file diff --git a/WindowsDevicePortalWrapper/Interfaces/IDevicePortalConnection.cs b/WindowsDevicePortalWrapper/Interfaces/IDevicePortalConnection.cs new file mode 100644 index 0000000..ee679d7 --- /dev/null +++ b/WindowsDevicePortalWrapper/Interfaces/IDevicePortalConnection.cs @@ -0,0 +1,64 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Net; +#if !WINDOWS_UWP +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +#endif +using static Microsoft.Tools.WindowsDevicePortal.DevicePortal; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Interface for creating a connection with the device portal. + /// + public interface IDevicePortalConnection + { + /// + /// Gets the base uri (ex: http://localhost) used to communicate with the device. + /// + Uri Connection { get; } + + /// + /// Gets the base uri (ex: ws://localhost) used to communicate with web sockets on the device. + /// + Uri WebSocketConnection { get; } + + /// + /// Gets the credentials used when communicating with the device. + /// + NetworkCredential Credentials { get; } + + /// + /// Gets or sets the family of the device (ex: Windows.Holographic). + /// + string Family { get; set; } + + /// + /// Gets or sets information describing the operating system installed on the device. + /// + OperatingSystemInformation OsInfo { get; set; } + + /// + /// Updates the http security requirements for device communication. + /// + /// True if an https connection is required, false otherwise. + void UpdateConnection(bool requiresHttps); + + /// + /// Updates the connection details (IP address) and http security requirements used when communicating with the device. + /// + /// Object that describes the current network configuration. + /// True if an https connection is required, false otherwise. + /// True if the previous connection's port is to continue to be used, false otherwise. + void UpdateConnection( + IpConfiguration ipConfig, + bool requiresHttps, + bool preservePort); + } +} diff --git a/WindowsDevicePortalWrapper/License.txt b/WindowsDevicePortalWrapper/License.txt new file mode 100644 index 0000000..1577ee9 --- /dev/null +++ b/WindowsDevicePortalWrapper/License.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Microsoft Corporation + +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. \ No newline at end of file diff --git a/WindowsDevicePortalWrapper/RestDelete.cs b/WindowsDevicePortalWrapper/RestDelete.cs new file mode 100644 index 0000000..12e8549 --- /dev/null +++ b/WindowsDevicePortalWrapper/RestDelete.cs @@ -0,0 +1,65 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// .net 4.x implementation of HTTP DeleteAsync + /// + public partial class DevicePortal + { + /// + /// Submits the http delete request to the specified uri. + /// + /// The uri to which the delete request will be issued. + /// Task tracking HTTP completion + public async Task DeleteAsync(Uri uri) + { + MemoryStream dataStream = null; + + WebRequestHandler requestSettings = new WebRequestHandler(); + requestSettings.UseDefaultCredentials = false; + requestSettings.Credentials = this.deviceConnection.Credentials; + requestSettings.ServerCertificateValidationCallback = this.ServerCertificateValidation; + + using (HttpClient client = new HttpClient(requestSettings)) + { + this.ApplyHttpHeaders(client, HttpMethods.Delete); + + using (HttpResponseMessage response = await client.DeleteAsync(uri).ConfigureAwait(false)) + { + if (!response.IsSuccessStatusCode) + { + throw await DevicePortalException.CreateAsync(response); + } + + this.RetrieveCsrfToken(response); + + if (response.Content != null) + { + using (HttpContent content = response.Content) + { + dataStream = new MemoryStream(); + + await content.CopyToAsync(dataStream).ConfigureAwait(false); + + // Ensure we return with the stream pointed at the origin. + dataStream.Position = 0; + } + } + } + } + + return dataStream; + } + } +} diff --git a/WindowsDevicePortalWrapper/RestGet.cs b/WindowsDevicePortalWrapper/RestGet.cs new file mode 100644 index 0000000..78dc22a --- /dev/null +++ b/WindowsDevicePortalWrapper/RestGet.cs @@ -0,0 +1,62 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// .net 4.x implementation of HTTP GetAsync + /// + public partial class DevicePortal + { + /// + /// Submits the http get request to the specified uri. + /// + /// The uri to which the get request will be issued. + /// Response data as a stream. + public async Task GetAsync( + Uri uri) + { + MemoryStream dataStream = null; + + WebRequestHandler handler = new WebRequestHandler(); + handler.UseDefaultCredentials = false; + handler.Credentials = this.deviceConnection.Credentials; + handler.ServerCertificateValidationCallback = this.ServerCertificateValidation; + + using (HttpClient client = new HttpClient(handler)) + { + this.ApplyHttpHeaders(client, HttpMethods.Get); + + using (HttpResponseMessage response = await client.GetAsync(uri).ConfigureAwait(false)) + { + if (!response.IsSuccessStatusCode) + { + throw await DevicePortalException.CreateAsync(response); + } + + this.RetrieveCsrfToken(response); + + using (HttpContent content = response.Content) + { + dataStream = new MemoryStream(); + + await content.CopyToAsync(dataStream).ConfigureAwait(false); + + // Ensure we return with the stream pointed at the origin. + dataStream.Position = 0; + } + } + } + + return dataStream; + } + } +} diff --git a/WindowsDevicePortalWrapper/RestPost.cs b/WindowsDevicePortalWrapper/RestPost.cs new file mode 100644 index 0000000..e2ad141 --- /dev/null +++ b/WindowsDevicePortalWrapper/RestPost.cs @@ -0,0 +1,92 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// .net 4.x implementation of HTTP PostAsync + /// + public partial class DevicePortal + { + /// + /// Submits the http post request to the specified uri. + /// + /// The uri to which the post request will be issued. + /// Optional stream containing data for the request body. + /// The type of that request body data. + /// Task tracking the completion of the POST request + public async Task PostAsync( + Uri uri, + Stream requestStream = null, + string requestStreamContentType = null) + { + StreamContent requestContent = null; + + if (requestStream != null) + { + requestContent = new StreamContent(requestStream); + requestContent.Headers.Remove(ContentTypeHeaderName); + requestContent.Headers.TryAddWithoutValidation(ContentTypeHeaderName, requestStreamContentType); + } + + return await this.PostAsync(uri, requestContent); + } + + /// + /// Submits the http post request to the specified uri. + /// + /// The uri to which the post request will be issued. + /// Optional content containing data for the request body. + /// Task tracking the completion of the POST request + public async Task PostAsync( + Uri uri, + HttpContent requestContent) + { + MemoryStream responseDataStream = null; + + WebRequestHandler requestSettings = new WebRequestHandler(); + requestSettings.UseDefaultCredentials = false; + requestSettings.Credentials = this.deviceConnection.Credentials; + requestSettings.ServerCertificateValidationCallback = this.ServerCertificateValidation; + + using (HttpClient client = new HttpClient(requestSettings)) + { + this.ApplyHttpHeaders(client, HttpMethods.Post); + + using (HttpResponseMessage response = await client.PostAsync(uri, requestContent).ConfigureAwait(false)) + { + if (!response.IsSuccessStatusCode) + { + throw await DevicePortalException.CreateAsync(response); + } + + this.RetrieveCsrfToken(response); + + if (response.Content != null) + { + using (HttpContent responseContent = response.Content) + { + responseDataStream = new MemoryStream(); + + await responseContent.CopyToAsync(responseDataStream).ConfigureAwait(false); + + // Ensure we return with the stream pointed at the origin. + responseDataStream.Position = 0; + } + } + } + } + + return responseDataStream; + } + } +} diff --git a/WindowsDevicePortalWrapper/RestPut.cs b/WindowsDevicePortalWrapper/RestPut.cs new file mode 100644 index 0000000..a85ed32 --- /dev/null +++ b/WindowsDevicePortalWrapper/RestPut.cs @@ -0,0 +1,69 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// .net 4.x implementation of HTTP PutAsync + /// + public partial class DevicePortal + { + /// + /// Submits the http put request to the specified uri. + /// + /// The uri to which the put request will be issued. + /// The HTTP content comprising the body of the request. + /// Task tracking the PUT completion. + public async Task PutAsync( + Uri uri, + HttpContent body = null) + { + MemoryStream dataStream = null; + + WebRequestHandler requestSettings = new WebRequestHandler(); + requestSettings.UseDefaultCredentials = false; + requestSettings.Credentials = this.deviceConnection.Credentials; + requestSettings.ServerCertificateValidationCallback = this.ServerCertificateValidation; + + using (HttpClient client = new HttpClient(requestSettings)) + { + this.ApplyHttpHeaders(client, HttpMethods.Put); + + // Send the request + using (HttpResponseMessage response = await client.PutAsync(uri, body).ConfigureAwait(false)) + { + if (!response.IsSuccessStatusCode) + { + throw await DevicePortalException.CreateAsync(response); + } + + this.RetrieveCsrfToken(response); + + if (response.Content != null) + { + using (HttpContent content = response.Content) + { + dataStream = new MemoryStream(); + + await content.CopyToAsync(dataStream).ConfigureAwait(false); + + // Ensure we return with the stream pointed at the origin. + dataStream.Position = 0; + } + } + } + } + + return dataStream; + } + } +} diff --git a/WindowsDevicePortalWrapper/UnvalidatedCert.cs b/WindowsDevicePortalWrapper/UnvalidatedCert.cs new file mode 100644 index 0000000..01ea01f --- /dev/null +++ b/WindowsDevicePortalWrapper/UnvalidatedCert.cs @@ -0,0 +1,21 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Handler for when an unvalidated cert is received. + /// + /// The sender of the event. + /// The server's certificate. + /// The cert chain. + /// Policy Errors. + /// whether the cert should still pass validation. + public delegate bool UnvalidatedCertEventHandler(DevicePortal sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors); +} diff --git a/WindowsDevicePortalWrapper/Utilities.cs b/WindowsDevicePortalWrapper/Utilities.cs new file mode 100644 index 0000000..a45a50a --- /dev/null +++ b/WindowsDevicePortalWrapper/Utilities.cs @@ -0,0 +1,107 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using static Microsoft.Tools.WindowsDevicePortal.DevicePortal; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// Utility class for common functions + /// + internal static class Utilities + { + /// + /// Constructs a fully formed REST API endpoint uri. + /// + /// The base uri (typically, just scheme and authority). + /// The path to the REST API method (ex: api/control/restart). + /// Parameterized data required by the REST API. + /// Uri object containing the complete path and query string required to issue the REST API call. + public static Uri BuildEndpoint( + Uri baseUri, + string path, + string payload = null) + { + string relativePart = !string.IsNullOrWhiteSpace(payload) ? + string.Format("{0}?{1}", path, payload) : path; + return new Uri(baseUri, relativePart); + } + + /// + /// Builds a query string from key value pairs. + /// + /// The key value pairs containing the query parameters. + /// Properly formatted query string. + public static string BuildQueryString(Dictionary payload) + { + string query = string.Empty; + + foreach (KeyValuePair pair in payload) + { + query += pair.Key + "=" + pair.Value + "&"; + } + + // Trim off the final ampersand. + query = query.Trim('&'); + + return query; + } + + /// + /// Encodes the specified string as a base64 value. + /// + /// The string to encode. + /// Base64 encoded version of the string data. + public static string Hex64Encode(string str) + { + return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(str)); + } + + /// + /// Checks if this device is a hololens. + /// + /// The platform. + /// The device family. + /// Whether this is a hololens. + public static bool IsHoloLens( + DevicePortalPlatforms platform, + string deviceFamily) + { + bool isHoloLens = false; + + if ((platform == DevicePortalPlatforms.HoloLens) || + ((platform == DevicePortalPlatforms.VirtualMachine) && (deviceFamily == "Windows.Holographic"))) + { + isHoloLens = true; + } + + return isHoloLens; + } + + /// + /// Modifies an endpoint to match the way we store it in a file. + /// This involves replacing a number of characters with underscores. + /// + /// The endpoint that is being modified. + public static void ModifyEndpointForFilename(ref string endpoint) + { + char[] invalidChars = Path.GetInvalidFileNameChars(); + + foreach (char character in invalidChars) + { + endpoint = endpoint.Replace(character, '_'); + } + + endpoint = endpoint.Replace('-', '_'); + endpoint = endpoint.Replace('.', '_'); + endpoint = endpoint.Replace('=', '_'); + endpoint = endpoint.Replace('&', '_'); + } + } +} \ No newline at end of file diff --git a/WindowsDevicePortalWrapper/WebSocket.cs b/WindowsDevicePortalWrapper/WebSocket.cs new file mode 100644 index 0000000..14de4cd --- /dev/null +++ b/WindowsDevicePortalWrapper/WebSocket.cs @@ -0,0 +1,220 @@ +//---------------------------------------------------------------------------------------------- +// +// Licensed under the MIT License. See LICENSE.TXT in the project root license information. +// +//---------------------------------------------------------------------------------------------- + +using System; +using System.IO; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Net.WebSockets; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// HTTP Websocket Wrapper + /// + /// Return type for the websocket messages. + internal partial class WebSocket + { + /// + /// The hresult for the connection being reset by the peer. + /// + private const int WSAECONNRESET = 0x2746; + + /// + /// The maximum number of bytes that can be received in a single chunk. + /// + private static readonly uint MaxChunkSizeInBytes = 1024; + + /// + /// The that is being wrapped. + /// + private ClientWebSocket websocket; + + /// + /// for receiving messages. + /// + private Task receivingMessagesTask; + + /// + /// The handler used to validate server certificates. + /// + private Func serverCertificateValidationHandler; + + /// + /// Initializes a new instance of the class. + /// + /// Implementation of a connection object. + /// Server certificate handler. + /// specifies whether the web socket should send streams (useful for creating mock data). + public WebSocket(IDevicePortalConnection connection, Func serverCertificateValidationHandler, bool sendStreams = false) + { + this.sendStreams = sendStreams; + this.deviceConnection = connection; + this.IsListeningForMessages = false; + this.serverCertificateValidationHandler = serverCertificateValidationHandler; + } + + /// + /// Opens a connection to the specified websocket API. + /// + /// The uri that the weboscket should connect to + /// The task of opening a connection to the websocket. + private async Task ConnectInternalAsync( + Uri endpoint) + { + this.websocket = new ClientWebSocket(); + this.websocket.Options.UseDefaultCredentials = false; + this.websocket.Options.Credentials = this.deviceConnection.Credentials; + this.websocket.Options.SetRequestHeader("Origin", this.deviceConnection.Connection.AbsoluteUri); + + // There is no way to set a ServerCertificateValidationCallback for a single web socket, hence the workaround. + ServicePointManager.ServerCertificateValidationCallback = delegate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors policyErrors) + { + return this.serverCertificateValidationHandler(sender, cert, chain, policyErrors); + }; + + await this.websocket.ConnectAsync(endpoint, CancellationToken.None); + this.IsConnected = true; + } + + /// + /// Closes the connection to the websocket. + /// + /// The task of closing the websocket connection. + private async Task CloseInternalAsync() + { + await Task.Run(() => + { + this.websocket.Dispose(); + this.websocket = null; + this.IsConnected = false; + }); + } + + /// + /// Stops listening for messages. + /// + /// The task of closing the websocket connection. + private async Task StopListeningForMessagesInternalAsync() + { + await this.websocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + + // Wait for web socket to no longer be receiving messages. + if (this.IsListeningForMessages) + { + await this.receivingMessagesTask; + this.receivingMessagesTask = null; + } + } + + /// + /// Starts listening for messages from the websocket. Once they are received they are parsed and the WebSocketMessageReceived event is raised. + /// + /// The task of listening for messages from the websocket. + private async Task StartListeningForMessagesInternalAsync() + { + await Task.Run(() => + { + this.StartListeningForMessagesInternal(); + }); + } + + /// + /// Starts listening for messages from the websocket. Once they are received they are parsed and the WebSocketMessageReceived event is raised. + /// + private void StartListeningForMessagesInternal() + { + this.IsListeningForMessages = true; + + this.receivingMessagesTask = Task.Run(async () => + { + try + { + ArraySegment buffer = new ArraySegment(new byte[MaxChunkSizeInBytes]); + + // Once close message is sent do not try to get any more messages as WDP will abort the web socket connection. + while (this.websocket.State == WebSocketState.Open) + { + // Receive single message in chunks. + using (var ms = new MemoryStream()) + { + WebSocketReceiveResult result; + + do + { + result = await this.websocket.ReceiveAsync(buffer, CancellationToken.None).ConfigureAwait(false); + + if (result.MessageType == WebSocketMessageType.Close) + { + await this.websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + return; + } + + if (result.Count > MaxChunkSizeInBytes) + { + throw new InvalidOperationException("Buffer not large enough"); + } + + ms.Write(buffer.Array, buffer.Offset, result.Count); + } + while (!result.EndOfMessage); + + ms.Seek(0, SeekOrigin.Begin); + + if (result.MessageType == WebSocketMessageType.Text) + { + Stream stream = new MemoryStream(); + + await ms.CopyToAsync(stream); + + // Ensure we return with the stream pointed at the origin. + stream.Position = 0; + + this.ConvertStreamToMessage(stream); + } + } + } + } + catch (WebSocketException e) + { + // If WDP aborted the web socket connection ignore the exception. + SocketException socketException = e.InnerException?.InnerException as SocketException; + if (socketException != null) + { + if (socketException.NativeErrorCode == WSAECONNRESET) + { + return; + } + } + + throw; + } + finally + { + this.IsListeningForMessages = false; + } + }); + } + + /// + /// Sends a message to the server. + /// + /// The message to send. + /// The task of sending a message to the websocket. + private async Task SendMessageInternalAsync(string message) + { + byte[] bytes = Encoding.UTF8.GetBytes(message); + ArraySegment buffer = new ArraySegment(bytes); + + await this.websocket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None); + } + } +}