From e712ad0f2095dfc16db0a934b228ec32b38c3e72 Mon Sep 17 00:00:00 2001 From: Phil Scott Date: Tue, 9 Apr 2024 23:43:12 -0400 Subject: [PATCH] Adding dedicated NativeAOT project Adding a nativeAot version of the demo application. While I was in here I fixed a few bugs in the original Demo though. --- examples/Cli/Demo/Commands/Run/RunCommand.cs | 1 - .../Cli/Demo/Commands/Serve/ServeCommand.cs | 6 +- examples/Cli/Demo/Demo.csproj | 5 +- examples/Cli/Demo/Program.cs | 18 ++--- examples/Cli/Demo/Utilities/SettingsDumper.cs | 8 +-- examples/Cli/Demo/Verbosity.cs | 7 -- .../DemoAot/Commands/Add/AddPackageCommand.cs | 46 +++++++++++++ .../Commands/Add/AddReferenceCommand.cs | 35 ++++++++++ .../Cli/DemoAot/Commands/Add/AddSettings.cs | 11 +++ .../Cli/DemoAot/Commands/Run/RunCommand.cs | 69 +++++++++++++++++++ .../DemoAot/Commands/Serve/ServeCommand.cs | 42 +++++++++++ examples/Cli/DemoAot/DemoAot.csproj | 20 ++++++ examples/Cli/DemoAot/Program.cs | 48 +++++++++++++ .../Cli/DemoAot/Utilities/SettingsDumper.cs | 30 ++++++++ examples/Cli/DemoAot/Verbosity.cs | 59 ++++++++++++++++ examples/Cli/Injection/Injection.csproj | 4 -- examples/Examples.sln | 15 ++++ 17 files changed, 389 insertions(+), 35 deletions(-) create mode 100644 examples/Cli/DemoAot/Commands/Add/AddPackageCommand.cs create mode 100644 examples/Cli/DemoAot/Commands/Add/AddReferenceCommand.cs create mode 100644 examples/Cli/DemoAot/Commands/Add/AddSettings.cs create mode 100644 examples/Cli/DemoAot/Commands/Run/RunCommand.cs create mode 100644 examples/Cli/DemoAot/Commands/Serve/ServeCommand.cs create mode 100644 examples/Cli/DemoAot/DemoAot.csproj create mode 100644 examples/Cli/DemoAot/Program.cs create mode 100644 examples/Cli/DemoAot/Utilities/SettingsDumper.cs create mode 100644 examples/Cli/DemoAot/Verbosity.cs diff --git a/examples/Cli/Demo/Commands/Run/RunCommand.cs b/examples/Cli/Demo/Commands/Run/RunCommand.cs index 9ffda5a07..a1aa675e4 100644 --- a/examples/Cli/Demo/Commands/Run/RunCommand.cs +++ b/examples/Cli/Demo/Commands/Run/RunCommand.cs @@ -1,5 +1,4 @@ using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using Demo.Utilities; using Spectre.Console.Cli; diff --git a/examples/Cli/Demo/Commands/Serve/ServeCommand.cs b/examples/Cli/Demo/Commands/Serve/ServeCommand.cs index 58d127101..311a31ffd 100644 --- a/examples/Cli/Demo/Commands/Serve/ServeCommand.cs +++ b/examples/Cli/Demo/Commands/Serve/ServeCommand.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using Demo.Utilities; +using Spectre.Console; using Spectre.Console.Cli; namespace Demo.Commands.Serve; @@ -11,6 +12,7 @@ public sealed class ServeCommand : Command public sealed class Settings : CommandSettings { [CommandOption("-p|--port ")] + [DefaultValue(8080)] [Description("Port to use. Defaults to [grey]8080[/]. Use [grey]0[/] for a dynamic port.")] public int Port { get; set; } @@ -26,11 +28,11 @@ public override int Execute(CommandContext context, Settings settings) var browser = settings.OpenBrowser.Value; if (browser != null) { - Console.WriteLine($"Open in {browser}"); + AnsiConsole.WriteLine($"Open in {browser}"); } else { - Console.WriteLine($"Open in default browser."); + AnsiConsole.WriteLine($"Open in default browser."); } } diff --git a/examples/Cli/Demo/Demo.csproj b/examples/Cli/Demo/Demo.csproj index e9732e858..8e4f05c73 100644 --- a/examples/Cli/Demo/Demo.csproj +++ b/examples/Cli/Demo/Demo.csproj @@ -9,10 +9,7 @@ Cli false - - true - false - + diff --git a/examples/Cli/Demo/Program.cs b/examples/Cli/Demo/Program.cs index 7a3b18d2a..f6c340329 100644 --- a/examples/Cli/Demo/Program.cs +++ b/examples/Cli/Demo/Program.cs @@ -1,5 +1,3 @@ -using System; -using System.Diagnostics.CodeAnalysis; using Demo.Commands; using Demo.Commands.Add; using Demo.Commands.Run; @@ -8,7 +6,6 @@ namespace Demo; - public static class Program { public static int Main(string[] args) @@ -16,30 +13,27 @@ public static int Main(string[] args) var app = new CommandApp(); app.Configure(config => { - config.PropagateExceptions(); config.SetApplicationName("fake-dotnet"); config.ValidateExamples(); config.AddExample("run", "--no-build"); // Run - config.AddCommand("run"); + config.AddCommand("run"); // Add config.AddBranch("add", add => { add.SetDescription("Add a package or reference to a .NET project"); - add.AddCommand("package"); - add.AddCommand("reference"); + add.AddCommand("package"); + add.AddCommand("reference"); }); // Serve - config.AddCommand("serve") + config.AddCommand("serve") .WithExample("serve", "-o", "firefox") .WithExample("serve", "--port", "80", "-o", "firefox"); - }); - - app.Run(args); + }); - return 0; + return app.Run(args); } } diff --git a/examples/Cli/Demo/Utilities/SettingsDumper.cs b/examples/Cli/Demo/Utilities/SettingsDumper.cs index 899d56477..562ae51d7 100644 --- a/examples/Cli/Demo/Utilities/SettingsDumper.cs +++ b/examples/Cli/Demo/Utilities/SettingsDumper.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Spectre.Console; using Spectre.Console.Cli; @@ -6,19 +5,18 @@ namespace Demo.Utilities; public static class SettingsDumper { - public static void Dump<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T settings) - where T : CommandSettings + public static void Dump(CommandSettings settings) { var table = new Table().RoundedBorder(); table.AddColumn("[grey]Name[/]"); table.AddColumn("[grey]Value[/]"); - var properties = typeof(T).GetProperties(); + var properties = settings.GetType().GetProperties(); foreach (var property in properties) { var value = property.GetValue(settings) ?.ToString() - ?.Replace("[", "[["); + ?.EscapeMarkup(); table.AddRow( property.Name, diff --git a/examples/Cli/Demo/Verbosity.cs b/examples/Cli/Demo/Verbosity.cs index dbc9315fa..84fc3b471 100644 --- a/examples/Cli/Demo/Verbosity.cs +++ b/examples/Cli/Demo/Verbosity.cs @@ -37,13 +37,6 @@ public VerbosityConverter() public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - // NativeAOT will pass an integer when using the DefaultValue and an enum, so we need to manually convert - // that back to the enum - if (value is int intValue && Enum.IsDefined(typeof(Verbosity), intValue)) - { - return (Verbosity)intValue; - } - if (value is string stringValue) { var result = _lookup.TryGetValue(stringValue, out var verbosity); diff --git a/examples/Cli/DemoAot/Commands/Add/AddPackageCommand.cs b/examples/Cli/DemoAot/Commands/Add/AddPackageCommand.cs new file mode 100644 index 000000000..1bb1f7387 --- /dev/null +++ b/examples/Cli/DemoAot/Commands/Add/AddPackageCommand.cs @@ -0,0 +1,46 @@ +using System.ComponentModel; +using DemoAot.Utilities; +using Spectre.Console.Cli; + +namespace DemoAot.Commands.Add; + +[Description("Add a NuGet package reference to the project.")] +public sealed class AddPackageCommand : Command +{ + public sealed class Settings : AddSettings + { + [CommandArgument(0, "")] + [Description("The package reference to add.")] + public string PackageName { get; set; } + + [CommandOption("-v|--version ")] + [Description("The version of the package to add.")] + public string Version { get; set; } + + [CommandOption("-f|--framework ")] + [Description("Add the reference only when targeting a specific framework.")] + public string Framework { get; set; } + + [CommandOption("--no-restore")] + [Description("Add the reference without performing restore preview and compatibility check.")] + public bool NoRestore { get; set; } + + [CommandOption("--source ")] + [Description("The NuGet package source to use during the restore.")] + public string Source { get; set; } + + [CommandOption("--package-directory ")] + [Description("The directory to restore packages to.")] + public string PackageDirectory { get; set; } + + [CommandOption("--interactive")] + [Description("Allows the command to stop and wait for user input or action (for example to complete authentication).")] + public bool Interactive { get; set; } + } + + public override int Execute(CommandContext context, Settings settings) + { + SettingsDumper.Dump(settings); + return 0; + } +} diff --git a/examples/Cli/DemoAot/Commands/Add/AddReferenceCommand.cs b/examples/Cli/DemoAot/Commands/Add/AddReferenceCommand.cs new file mode 100644 index 000000000..f427e8a60 --- /dev/null +++ b/examples/Cli/DemoAot/Commands/Add/AddReferenceCommand.cs @@ -0,0 +1,35 @@ +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using DemoAot.Utilities; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace DemoAot.Commands.Add; + +public sealed class AddReferenceCommand : Command +{ + public sealed class Settings : AddSettings + { + [CommandArgument(0, "")] + [Description("The package reference to add.")] + public DirectoryInfo ProjectPath { get; set; } + + [CommandOption("-f|--framework ")] + [Description("Add the reference only when targeting a specific framework.")] + public string Framework { get; set; } + + [CommandOption("--interactive")] + [Description("Allows the command to stop and wait for user input or action (for example to complete authentication).")] + public bool Interactive { get; set; } + } + + // In non-AOT scenarios, we can dynamically call the constructor to DirectoryInfo via reflection. + // With trimming enabled we need to be explicit about requiring that constructor. + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(DirectoryInfo))] + public override int Execute(CommandContext context, Settings settings) + { + SettingsDumper.Dump(settings); + return 0; + } +} diff --git a/examples/Cli/DemoAot/Commands/Add/AddSettings.cs b/examples/Cli/DemoAot/Commands/Add/AddSettings.cs new file mode 100644 index 000000000..c5a013c8c --- /dev/null +++ b/examples/Cli/DemoAot/Commands/Add/AddSettings.cs @@ -0,0 +1,11 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace DemoAot.Commands.Add; + +public abstract class AddSettings : CommandSettings +{ + [CommandArgument(0, "")] + [Description("The project file to operate on. If a file is not specified, the command will search the current directory for one.")] + public string Project { get; set; } +} diff --git a/examples/Cli/DemoAot/Commands/Run/RunCommand.cs b/examples/Cli/DemoAot/Commands/Run/RunCommand.cs new file mode 100644 index 000000000..a37a35787 --- /dev/null +++ b/examples/Cli/DemoAot/Commands/Run/RunCommand.cs @@ -0,0 +1,69 @@ +using System.ComponentModel; +using DemoAot.Utilities; +using Spectre.Console.Cli; + +namespace DemoAot.Commands.Run; + +[Description("Build and run a .NET project output.")] +public sealed class RunCommand : Command +{ + public sealed class Settings : CommandSettings + { + [CommandOption("-c|--configuration ")] + [Description("The configuration to run for. The default for most projects is '[grey]Debug[/]'.")] + [DefaultValue("Debug")] + public string Configuration { get; set; } + + [CommandOption("-f|--framework ")] + [Description("The target framework to run for. The target framework must also be specified in the project file.")] + public string Framework { get; set; } + + [CommandOption("-r|--runtime ")] + [Description("The target runtime to run for.")] + public string RuntimeIdentifier { get; set; } + + [CommandOption("-p|--project ")] + [Description("The path to the project file to run (defaults to the current directory if there is only one project).")] + public string ProjectPath { get; set; } + + [CommandOption("--launch-profile ")] + [Description("The name of the launch profile (if any) to use when launching the application.")] + public string LaunchProfile { get; set; } + + [CommandOption("--no-launch-profile")] + [Description("Do not attempt to use [grey]launchSettings.json[/] to configure the application.")] + public bool NoLaunchProfile { get; set; } + + [CommandOption("--no-build")] + [Description("Do not build the project before running. Implies [grey]--no-restore[/].")] + public bool NoBuild { get; set; } + + [CommandOption("--interactive")] + [Description("Allows the command to stop and wait for user input or action (for example to complete authentication).")] + public string Interactive { get; set; } + + [CommandOption("--no-restore")] + [Description("Do not restore the project before building.")] + public bool NoRestore { get; set; } + + [CommandOption("--verbosity ")] + [Description("Set the MSBuild verbosity level. Allowed values are q[grey]uiet[/], m[grey]inimal[/], n[grey]ormal[/], d[grey]etailed[/], and diag[grey]nostic[/].")] + [TypeConverter(typeof(VerbosityConverter))] + [DefaultValue(Verbosity.Normal)] + public Verbosity Verbosity { get; set; } + + [CommandOption("--no-dependencies")] + [Description("Do not restore project-to-project references and only restore the specified project.")] + public bool NoDependencies { get; set; } + + [CommandOption("--force")] + [Description("Force all dependencies to be resolved even if the last restore was successful. This is equivalent to deleting [grey]project.assets.json[/].")] + public bool Force { get; set; } + } + + public override int Execute(CommandContext context, Settings settings) + { + SettingsDumper.Dump(settings); + return 0; + } +} diff --git a/examples/Cli/DemoAot/Commands/Serve/ServeCommand.cs b/examples/Cli/DemoAot/Commands/Serve/ServeCommand.cs new file mode 100644 index 000000000..51a3467d2 --- /dev/null +++ b/examples/Cli/DemoAot/Commands/Serve/ServeCommand.cs @@ -0,0 +1,42 @@ +using System; +using System.ComponentModel; +using DemoAot.Utilities; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace DemoAot.Commands.Serve; + +[Description("Launches a web server in the current working directory and serves all files in it.")] +public sealed class ServeCommand : Command +{ + public sealed class Settings : CommandSettings + { + [CommandOption("-p|--port ")] + [DefaultValue(8080)] + [Description("Port to use. Defaults to [grey]8080[/]. Use [grey]0[/] for a dynamic port.")] + public int Port { get; set; } + + [CommandOption("-o|--open-browser [BROWSER]")] + [Description("Open a web browser when the server starts. You can also specify which browser to use. If none is specified, the default one will be used.")] + public FlagValue OpenBrowser { get; set; } + } + + public override int Execute(CommandContext context, Settings settings) + { + if (settings.OpenBrowser.IsSet) + { + var browser = settings.OpenBrowser.Value; + if (browser != null) + { + AnsiConsole.WriteLine($"Open in {browser}"); + } + else + { + AnsiConsole.WriteLine($"Open in default browser."); + } + } + + SettingsDumper.Dump(settings); + return 0; + } +} diff --git a/examples/Cli/DemoAot/DemoAot.csproj b/examples/Cli/DemoAot/DemoAot.csproj new file mode 100644 index 000000000..0eb020067 --- /dev/null +++ b/examples/Cli/DemoAot/DemoAot.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + false + DemoAot + Demonstrates the most common use cases of Spectre.Cli, configured for NativeAOT. + Cli + false + + + true + false + + + + + + diff --git a/examples/Cli/DemoAot/Program.cs b/examples/Cli/DemoAot/Program.cs new file mode 100644 index 000000000..a22d5ea56 --- /dev/null +++ b/examples/Cli/DemoAot/Program.cs @@ -0,0 +1,48 @@ +using System; +using DemoAot.Commands.Add; +using DemoAot.Commands.Run; +using DemoAot.Commands.Serve; +using Spectre.Console; +using Spectre.Console.Cli; + +try +{ + var app = new CommandApp(); + app.Configure(config => + { + config.PropagateExceptions(); + config.SetApplicationName("fake-dotnet"); + config.ValidateExamples(); + config.AddExample("run", "--no-build"); + + // Run + config.AddCommand("run"); + + // Add + config.AddBranch("add", add => + { + add.SetDescription("Add a package or reference to a .NET project"); + add.AddCommand("package"); + add.AddCommand("reference"); + }); + + // Serve + config.AddCommand("serve") + .WithExample("serve", "-o", "firefox") + .WithExample("serve", "--port", "80", "-o", "firefox"); + }); + + app.Run(args); + + return 0; +} +catch (Exception e) +{ + // this will raise a warning because AnsiConsole.WriteException relies on reflection to generate the pretty formatted + // exception. When executed in a NativeAOT scenario it is the same as calling e.ToString() + #pragma warning disable IL2026 + AnsiConsole.WriteException(e); + #pragma warning restore IL2026 + + return -1; +} diff --git a/examples/Cli/DemoAot/Utilities/SettingsDumper.cs b/examples/Cli/DemoAot/Utilities/SettingsDumper.cs new file mode 100644 index 000000000..2aa3ab097 --- /dev/null +++ b/examples/Cli/DemoAot/Utilities/SettingsDumper.cs @@ -0,0 +1,30 @@ +using System.Diagnostics.CodeAnalysis; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace DemoAot.Utilities; + +public static class SettingsDumper +{ + public static void Dump<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T settings) + where T : CommandSettings + { + var table = new Table().RoundedBorder(); + table.AddColumn("[grey]Name[/]"); + table.AddColumn("[grey]Value[/]"); + + var properties = typeof(T).GetProperties(); + foreach (var property in properties) + { + var value = property.GetValue(settings) + ?.ToString() + ?.EscapeMarkup(); + + table.AddRow( + property.Name, + value ?? "[grey]null[/]"); + } + + AnsiConsole.Write(table); + } +} diff --git a/examples/Cli/DemoAot/Verbosity.cs b/examples/Cli/DemoAot/Verbosity.cs new file mode 100644 index 000000000..23ca952b5 --- /dev/null +++ b/examples/Cli/DemoAot/Verbosity.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +namespace DemoAot; + +public enum Verbosity +{ + Quiet, + Minimal, + Normal, + Detailed, + Diagnostic +} + +public sealed class VerbosityConverter : TypeConverter +{ + private readonly Dictionary _lookup; + + public VerbosityConverter() + { + _lookup = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "q", Verbosity.Quiet }, + { "quiet", Verbosity.Quiet }, + { "m", Verbosity.Minimal }, + { "minimal", Verbosity.Minimal }, + { "n", Verbosity.Normal }, + { "normal", Verbosity.Normal }, + { "d", Verbosity.Detailed }, + { "detailed", Verbosity.Detailed }, + { "diag", Verbosity.Diagnostic }, + { "diagnostic", Verbosity.Diagnostic } + }; + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + // NativeAOT will pass an integer when using the DefaultValue and an enum, so we need to manually convert + // that back to the enum + if (value is int intValue && Enum.IsDefined(typeof(Verbosity), intValue)) + { + return (Verbosity)intValue; + } + + if (value is string stringValue) + { + var result = _lookup.TryGetValue(stringValue, out var verbosity); + if (!result) + { + const string format = "The value '{0}' is not a valid verbosity."; + var message = string.Format(CultureInfo.InvariantCulture, format, value); + throw new InvalidOperationException(message); + } + return verbosity; + } + throw new NotSupportedException("Can't convert value to verbosity."); + } +} diff --git a/examples/Cli/Injection/Injection.csproj b/examples/Cli/Injection/Injection.csproj index d404487fc..69e46476a 100644 --- a/examples/Cli/Injection/Injection.csproj +++ b/examples/Cli/Injection/Injection.csproj @@ -9,10 +9,6 @@ Cli false - - - true - diff --git a/examples/Examples.sln b/examples/Examples.sln index 316646633..2d835eec9 100644 --- a/examples/Examples.sln +++ b/examples/Examples.sln @@ -87,6 +87,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Help", "Cli\Help\Help.cspro EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Decorations", "Console\Decorations\Decorations.csproj", "{FC5852F1-E01F-4DF7-9B49-CA19A9EE670F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoAot", "Cli\DemoAot\DemoAot.csproj", "{D4C71A95-D487-4F43-A12B-18DF8D38B9D2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -577,6 +579,18 @@ Global {FC5852F1-E01F-4DF7-9B49-CA19A9EE670F}.Release|x64.Build.0 = Release|Any CPU {FC5852F1-E01F-4DF7-9B49-CA19A9EE670F}.Release|x86.ActiveCfg = Release|Any CPU {FC5852F1-E01F-4DF7-9B49-CA19A9EE670F}.Release|x86.Build.0 = Release|Any CPU + {D4C71A95-D487-4F43-A12B-18DF8D38B9D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4C71A95-D487-4F43-A12B-18DF8D38B9D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4C71A95-D487-4F43-A12B-18DF8D38B9D2}.Debug|x64.ActiveCfg = Debug|Any CPU + {D4C71A95-D487-4F43-A12B-18DF8D38B9D2}.Debug|x64.Build.0 = Debug|Any CPU + {D4C71A95-D487-4F43-A12B-18DF8D38B9D2}.Debug|x86.ActiveCfg = Debug|Any CPU + {D4C71A95-D487-4F43-A12B-18DF8D38B9D2}.Debug|x86.Build.0 = Debug|Any CPU + {D4C71A95-D487-4F43-A12B-18DF8D38B9D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4C71A95-D487-4F43-A12B-18DF8D38B9D2}.Release|Any CPU.Build.0 = Release|Any CPU + {D4C71A95-D487-4F43-A12B-18DF8D38B9D2}.Release|x64.ActiveCfg = Release|Any CPU + {D4C71A95-D487-4F43-A12B-18DF8D38B9D2}.Release|x64.Build.0 = Release|Any CPU + {D4C71A95-D487-4F43-A12B-18DF8D38B9D2}.Release|x86.ActiveCfg = Release|Any CPU + {D4C71A95-D487-4F43-A12B-18DF8D38B9D2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -593,6 +607,7 @@ Global {EFAADF6A-C77D-41EC-83F5-BBB4FFC5A6D7} = {2571F1BD-6556-4F96-B27B-B6190E1BF13A} {91A5637F-1F89-48B3-A0BA-6CC629807393} = {2571F1BD-6556-4F96-B27B-B6190E1BF13A} {BAB490D6-FF8D-462B-B2B0-933384D629DB} = {4682E9B7-B54C-419D-B92F-470DA4E5674C} + {D4C71A95-D487-4F43-A12B-18DF8D38B9D2} = {4682E9B7-B54C-419D-B92F-470DA4E5674C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3EE724C5-CAB4-410D-AC63-8D4260EF83ED}