From add305a55c9ed753d62e66cf6b0446c7ec202e71 Mon Sep 17 00:00:00 2001 From: Caio Cesar Saldanha Maia Orejuela Kinelski Date: Fri, 20 Jan 2017 15:25:14 -0800 Subject: [PATCH 1/2] Multi-level SharedFX lookup implemented. --- .../design-docs/multilevel-sharedfx-lookup.md | 140 +++++++++++++++++ src/corehost/cli/fxr/fx_muxer.cpp | 142 ++++++++++++------ src/corehost/cli/fxr/fx_muxer.h | 1 + 3 files changed, 240 insertions(+), 43 deletions(-) create mode 100644 Documentation/design-docs/multilevel-sharedfx-lookup.md diff --git a/Documentation/design-docs/multilevel-sharedfx-lookup.md b/Documentation/design-docs/multilevel-sharedfx-lookup.md new file mode 100644 index 0000000000..e8857eb2d9 --- /dev/null +++ b/Documentation/design-docs/multilevel-sharedfx-lookup.md @@ -0,0 +1,140 @@ +# Multi-level SharedFX Lookup + +## Introduction + +There are two possible ways of running .NET Core Applications: through dotnet.exe or through a custom executable appname.exe. The first one is used when the user wants to run a portable app or a .NET Core command while the second one is used for standalone applications. Both executables share exactly the same source code. + +The executable is in charge of finding and loading the hostfxr.dll file. The hostfxr, in turn, must find and load the hostpolicy.dll file (it’s also responsible for searching for the SDK when running .NET commands). At last the coreclr.dll file must be found and loaded by the hostpolicy. Standalone apps are supposed to keep all its dependencies in the same location as the executable. Portable apps must have the runtime files inside predefined folders. + +## Semantic Versioning 1.0.0 + +.NET Core uses the Semantic Versioning system to manage its version number. It’s important to understand how this system works because since it’s being proposed to search files from different locations, it’s necessary to establish the software behavior based on compatibility limitations. + +The version number must take the form X.Y.Z where X is the major version, Y is the minor version, and Z is the patch version. Bug fixes and modifications that do not affect the API itself must increment the patch version. Changes that affect the API but have backwards compatibility must increment the minor version and reset the patch version to zero. Finally changes that are backwards incompatible must increment the major version and reset both patch and minor versions to zero. + +It’s also possible to append a dash followed by a string after the version number to specify a pre-release. The string must be composed of only alphanumeric characters plus dash. Precedence is determined by lexicographic ASCII sort order. + +Versions that are not pre-releases are called productions. + + For instance, a valid Semantic Versioning number sort would be: + 1.0.0 -> 1.0.1 -> 1.0.1-alpha -> 1.1.0 -> 1.1.1 -> 2.0.0. + +## Executable + +The executable’s only task is to find and load the hostfxr.dll file and pass on its arguments. + +Portable applications are supposed to have version folders for hostfxr inside host\fxr directory close to dotnet.exe itself. The most recent version folder is picked by following the Semantic Versioning system described above. The hostfxr.dll file is expected to be inside the chosen folder. + +If the file cannot be found, then the user is probably trying to run a standalone application. The running program then searches for the hostfxr.dll file in the executable directory. + +It’s important to notice that, at this point, the process still does not make a distinction between portable and standalone apps. + +## Hostfxr + +### Host mode + +The hostfxr’s first task is to determine the running host mode. It’s a muxer if invoked as dotnet.exe, a standalone if invoked as appname.exe, or a splitfx if other conditions apply. Since the following changes will not interfere in the way that standalone and splitfx modes are handled, then it’s safe to assume that we will be dealing with a muxer. + +### SDK Search + +There are two possibilities for a muxer: it can be a portable app or a .NET Core command. + +In the first case the app file path should have been specified as an argument to the dotnet.exe. + +In the second case the dotnet.dll from SDK must be invoked as a portable app. At first the running program searches for the global.json file which may have specified a CLI version. It starts from the current working directory and looks for it inside all parent folder hierarchy. After that, it searches for the dotnet.dll file inside the sdk\CLI_version subfolder in the executable directory. If the version defined in the global.json file or the specified version folder cannot be found, then it must choose the most appropriate one. The most appropriate version is defined as the latest production version according to the Semantic Versioning system. If no production version is available, then the latest pre-release must be chosen. + +### Framework search and rolling forward + +The hostfxr then searches for the configuration files appname.runtimeconfig.json and appname.runtimeconfig.dev.json in the same folder as the appname.dll file. The first one contains the specified framework name and version that are necessary to find its folder. + +The shared\fxname subfolder in the executable directory is expected to contain some framework version folders. If the required version was passed as an argument to appname.exe, then the framework folder path is already decided. + +If the desired version was not passed as an argument, then the one in appname.runtimeconfig.json must be used as a starting point to determine which will be chosen. There are two possible scenarios: + +- If the version specified in the configuration file is a production, then the default behavior is to pick the latest available production that differs only in patch. +- If the version specified in the configuration file is a pre-release, then it will pick the exact specified version. If its version folder does not exist, then it will search for the smallest pre-release that is greater than the specified one. + +This process of choosing the most appropriate available version instead of the specified one is called “rolling forward”. + +Hostfxr must then locate the hostpolicy.dll file: + +- Portable apps are expected to have a file called fxname.deps.json inside the framework folder. This file contains information about the application’s dependencies and during most of the time it will be used by the hostpolicy. After locating the json file, the hostfxr must search inside it for what the specified hostpolicy version is. +- The pkgs\hostpolicy_version subfolder below the default servicing directory is expected to contain the hostpolicy.dll. +- If for any reason the file cannot be found, then the running program will search for the hostpolicy.dll file inside the framework folder independently of the version. +- Finally, if the file still cannot be found, it will try looking inside the probing paths passed as arguments to the process. + +The hostpolicy is then loaded into memory and executed. + +## Hostpolicy + +Hostpolicy is in charge of looking for all dependencies files required for the application. That includes the coreclr.dll file which is necessary to run it. + +It will look for the json files that specify the needed assemblies’ filenames: + +- If the appname.deps.json file path has not been specified as an argument, then it is expected to be inside the application directory. +- Portable apps are supposed to have an fxname.deps.json file inside the framework folder. + +Both files carry the filenames for dependencies that must be found. They can be categorized as runtime, native or resources assemblies. The coreclr.dll file is expected to be found during the native assemblies search. + +At last, the coreclr is loaded into memory and called to run the application. + +## Proposed changes + +Almost every file search is done in relation to the executable directory. It would be better to be able to search for some files in other directories as well. Suggested folders are the current working directory, the user location and the global .NET location. The user and global folders may vary depending on the running operational system. They are defined as follows: + +User location: + + Windows 32-bit: %SystemDrive%\Users\username\.dotnet\x86 + Windows 64-bit: %SystemDrive%\Users\username\.dotnet\x64 + Unix 32-bit: /home/username/.dotnet/x86 + Unix 64-bit: /home/username/.dotnet/x64 + +Global .NET location: + + Windows 32-bit: %SystemDrive%\Program Files\dotnet + Windows 64-bit (32-bit application): %SystemDrive%\Program Files (x86)\dotnet + Windows 64-bit (64-bit application): %SystemDrive%\Program Files\dotnet + Unix: the directory of “dotnet” defined in the system path. + +### Framework search + +It’s being proposed that, if the specified version is defined through the configuration json file, the search must be conducted as follows: + +- For productions: + + 1. In relation to the current working directory: search for the most appropriate version by rolling forward. If it cannot be found, proceed to the next step. + 2. In relation to the user location: search for the most appropriate version by rolling forward. If it cannot be found, proceed to the next step. + 3. In relation to the executable directory: search for the most appropriate version by rolling forward. If it cannot be found, proceed to the next step. + 4. In relation to the global location: search for the most appropriate version by rolling forward. If it cannot be found, then we were not able to locate any compatible version. + +- For pre-releases: + + 1. In relation to the current working directory: search for the specified version. If it cannot be found, search for the most appropriate version by rolling forward. If no compatible version can be found, proceed to the next step. + 2. In relation to the user location: search for the specified version. If it cannot be found, search for the most appropriate version by rolling forward. If no compatible version can be found, proceed to the next step. + 3. In relation to the executable directory: search for the specified version. If it cannot be found, search for the most appropriate version by rolling forward. If no compatible version can be found, proceed to the next step. + 4. In relation to the global location: search for the specified version. If it cannot be found, search for the most appropriate version by rolling forward. If no compatible version can be found, then we were not able to locate any compatible version. + +In the case that the desired version is defined through an argument, the multi-level lookup will happen as well but it will only consider the exact specified version (it will not roll forward). + +### Tests + +To make sure that the changes are working correctly, the following behavior conditions will be verified through tests: + +- Folders must be verified in the correct order. +- If production, then a roll forward must happen in a given folder before proceeding to the next one. +- If pre-release, then a roll forward must happen in a given folder only if the specified version is not found. If there is no compatible version available, then it must proceed to the next location. +- If the version is specified through an argument, then roll forwards are not allowed to happen. +- If no compatible version folder is found, then an error message must be returned and the process must end. + +## Future changes + +### SDK search + +By following similar logic, it will be possible to implement future changes in the SDK search. Instead of looking for it only in relation to the executable directory, we could do it in the folders specified above by following the same priority rank. + +The search would be conducted as follows: + +1. In relation to the current working directory: search for the specified version. If it cannot be found, choose the most appropriate available version. If there’s no available version, proceed to the next step. +2. In relation to the user location: search for the specified version. If it cannot be found, choose the most appropriate available version. If there’s no available version, proceed to the next step. +3. In relation to the executable directory: search for the specified version. If it cannot be found, choose the most appropriate available version. If there’s no available version, proceed to the next step. +4. In relation to the global location: search for the specified version. If it cannot be found, choose the most appropriate available version. If there’s no available version, then we were not able to find any version folder and an error message must be returned. \ No newline at end of file diff --git a/src/corehost/cli/fxr/fx_muxer.cpp b/src/corehost/cli/fxr/fx_muxer.cpp index d331ba9d4f..84a01ad036 100644 --- a/src/corehost/cli/fxr/fx_muxer.cpp +++ b/src/corehost/cli/fxr/fx_muxer.cpp @@ -310,6 +310,39 @@ bool fx_muxer_t::resolve_hostpolicy_dir(host_mode_t mode, return false; } +void fx_muxer_t::resolve_roll_forward(pal::string_t& fx_dir, const pal::string_t& fx_ver, const fx_ver_t& specified) +{ + trace::verbose(_X("Attempting FX roll forward starting from [%s]"), fx_ver.c_str()); + + std::vector list; + pal::readdir(fx_dir, &list); + fx_ver_t most_compatible = specified; + for (const auto& version : list) + { + trace::verbose(_X("Inspecting version... [%s]"), version.c_str()); + fx_ver_t ver(-1, -1, -1); + if (!specified.is_prerelease() && fx_ver_t::parse(version, &ver, true) && // true -- only prod. prevents roll forward to prerelease. + ver.get_major() == specified.get_major() && + ver.get_minor() == specified.get_minor()) + { + // Pick the greatest production that differs only in patch. + most_compatible = std::max(ver, most_compatible); + } + if (specified.is_prerelease() && fx_ver_t::parse(version, &ver, false) && // false -- implies both production and prerelease. + ver.is_prerelease() && // prevent roll forward to production. + ver.get_major() == specified.get_major() && + ver.get_minor() == specified.get_minor() && + ver.get_patch() == specified.get_patch() && + ver > specified) + { + // Pick the smallest prerelease that is greater than specified. + most_compatible = (most_compatible == specified) ? ver : std::min(ver, most_compatible); + } + } + pal::string_t most_compatible_str = most_compatible.as_str(); + append_path(&fx_dir, most_compatible_str.c_str()); +} + pal::string_t fx_muxer_t::resolve_fx_dir(host_mode_t mode, const pal::string_t& own_dir, const runtime_config_t& config, const pal::string_t& specified_fx_version) { // No FX resolution for standalone apps. @@ -322,7 +355,7 @@ pal::string_t fx_muxer_t::resolve_fx_dir(host_mode_t mode, const pal::string_t& } assert(mode == host_mode_t::muxer); - trace::verbose(_X("--- Resolving FX directory from muxer dir '%s', specified '%s'"), own_dir.c_str(), specified_fx_version.c_str()); + trace::verbose(_X("--- Resolving FX directory, specified '%s'"), specified_fx_version.c_str()); const auto fx_name = config.get_fx_name(); const auto fx_ver = specified_fx_version.empty() ? config.get_fx_version() : specified_fx_version; @@ -333,67 +366,90 @@ pal::string_t fx_muxer_t::resolve_fx_dir(host_mode_t mode, const pal::string_t& return pal::string_t(); } - auto fx_dir = own_dir; - append_path(&fx_dir, _X("shared")); - append_path(&fx_dir, fx_name.c_str()); + // Multi-level SharedFX lookup will look for the most appropriate version in several locations + // by following the priority rank below (from 1 to 4): + // 1. Current working directory + // 2. User directory + // 3. .exe directory + // 4. Global .NET directory + // If it is not activated, then only .exe directory will be considered + + std::vector hive_dir; + pal::string_t env_lookup; + + // Multi-level SharedFX lookup can be disabled by setting DOTNET_MULTILEVEL_LOOKUP env var to zero + bool multilevel_lookup = true; + if (pal::getenv(_X("DOTNET_MULTILEVEL_LOOKUP"), &env_lookup)) + { + auto env_val = pal::xtoi(env_lookup.c_str()); + multilevel_lookup = (env_val != 0); + } + + pal::string_t cwd; + pal::string_t local_dir; + pal::string_t global_dir; - bool do_roll_forward = false; - if (specified_fx_version.empty()) + if (multilevel_lookup) { - if (!specified.is_prerelease()) + if (pal::getcwd(&cwd)) { - // If production and no roll forward use given version. - do_roll_forward = config.get_patch_roll_fwd(); + hive_dir.push_back(cwd); } - else + if (pal::get_local_dotnet_dir(&local_dir)) { - // Prerelease, but roll forward only if version doesn't exist. - pal::string_t ver_dir = fx_dir; - append_path(&ver_dir, fx_ver.c_str()); - do_roll_forward = !pal::directory_exists(ver_dir); + hive_dir.push_back(local_dir); } } - - if (!do_roll_forward) + hive_dir.push_back(own_dir); + if (multilevel_lookup && pal::get_global_dotnet_dir(&global_dir)) { - trace::verbose(_X("Did not roll forward because specified version='%s', patch_roll_fwd=%d, chose [%s]"), specified_fx_version.c_str(), config.get_patch_roll_fwd(), fx_ver.c_str()); - append_path(&fx_dir, fx_ver.c_str()); + hive_dir.push_back(global_dir); } - else + + for (pal::string_t dir : hive_dir) { - trace::verbose(_X("Attempting FX roll forward starting from [%s]"), fx_ver.c_str()); + auto fx_dir = dir; + trace::verbose(_X("Searching FX directory in [%s]"), fx_dir.c_str()); + + append_path(&fx_dir, _X("shared")); + append_path(&fx_dir, fx_name.c_str()); - std::vector list; - pal::readdir(fx_dir, &list); - fx_ver_t most_compatible = specified; - for (const auto& version : list) + bool do_roll_forward = false; + if (specified_fx_version.empty()) { - trace::verbose(_X("Inspecting version... [%s]"), version.c_str()); - fx_ver_t ver(-1, -1, -1); - if (!specified.is_prerelease() && fx_ver_t::parse(version, &ver, true) && // true -- only prod. prevents roll forward to prerelease. - ver.get_major() == specified.get_major() && - ver.get_minor() == specified.get_minor()) + if (!specified.is_prerelease()) { - // Pick the greatest production that differs only in patch. - most_compatible = std::max(ver, most_compatible); + // If production and no roll forward use given version. + do_roll_forward = config.get_patch_roll_fwd(); } - if (specified.is_prerelease() && fx_ver_t::parse(version, &ver, false) && // false -- implies both production and prerelease. - ver.is_prerelease() && // prevent roll forward to production. - ver.get_major() == specified.get_major() && - ver.get_minor() == specified.get_minor() && - ver.get_patch() == specified.get_patch() && - ver > specified) + else { - // Pick the smallest prerelease that is greater than specified. - most_compatible = (most_compatible == specified) ? ver : std::min(ver, most_compatible); + // Prerelease, but roll forward only if version doesn't exist. + pal::string_t ver_dir = fx_dir; + append_path(&ver_dir, fx_ver.c_str()); + do_roll_forward = !pal::directory_exists(ver_dir); } } - pal::string_t most_compatible_str = most_compatible.as_str(); - append_path(&fx_dir, most_compatible_str.c_str()); + + if (!do_roll_forward) + { + trace::verbose(_X("Did not roll forward because specified version='%s', patch_roll_fwd=%d, chose [%s]"), specified_fx_version.c_str(), config.get_patch_roll_fwd(), fx_ver.c_str()); + append_path(&fx_dir, fx_ver.c_str()); + } + else + { + resolve_roll_forward(fx_dir, fx_ver, specified); + } + + if (pal::directory_exists(fx_dir)) + { + trace::verbose(_X("Chose FX version [%s]"), fx_dir.c_str()); + return fx_dir; + } } - trace::verbose(_X("Chose FX version [%s]"), fx_dir.c_str()); - return fx_dir; + trace::error(_X("It was not possible to find any compatible framework version")); + return pal::string_t(); } pal::string_t fx_muxer_t::resolve_cli_version(const pal::string_t& global_json) diff --git a/src/corehost/cli/fxr/fx_muxer.h b/src/corehost/cli/fxr/fx_muxer.h index 5766fddfca..2b5800b5c3 100644 --- a/src/corehost/cli/fxr/fx_muxer.h +++ b/src/corehost/cli/fxr/fx_muxer.h @@ -33,6 +33,7 @@ class fx_muxer_t const std::vector& probe_realpaths, const runtime_config_t& config, pal::string_t* impl_dir); + static void resolve_roll_forward(pal::string_t& fx_dir, const pal::string_t& fx_ver, const fx_ver_t& specified); static pal::string_t resolve_fx_dir(host_mode_t mode, const pal::string_t& own_dir, const runtime_config_t& config, const pal::string_t& specified_fx_version); static pal::string_t resolve_cli_version(const pal::string_t& global); static bool resolve_sdk_dotnet_path(const pal::string_t& own_dir, pal::string_t* cli_sdk); From 512a54dd32bec63388b56cb5bf6411f9497e1f50 Mon Sep 17 00:00:00 2001 From: Caio Cesar Saldanha Maia Orejuela Kinelski Date: Fri, 3 Feb 2017 17:31:40 -0800 Subject: [PATCH 2/2] SharedFxLookup Tests implemented. --- .../SharedFxLookupPortableApp/Program.cs | 16 + .../SharedFxLookupPortableApp.xproj | 19 + .../SharedFxLookupPortableApp/project.json | 402 +++++++++++++++++ ...pPortableApp_prerelease.runtimeconfig.json | 8 + ...pPortableApp_production.runtimeconfig.json | 8 + ...nThatICareAboutMultilevelSharedFxLookup.cs | 421 ++++++++++++++++++ 6 files changed, 874 insertions(+) create mode 100644 TestAssets/TestProjects/SharedFxLookupPortableApp/Program.cs create mode 100644 TestAssets/TestProjects/SharedFxLookupPortableApp/SharedFxLookupPortableApp.xproj create mode 100644 TestAssets/TestProjects/SharedFxLookupPortableApp/project.json create mode 100644 TestAssets/TestUtils/SharedFxLookup/SharedFxLookupPortableApp_prerelease.runtimeconfig.json create mode 100644 TestAssets/TestUtils/SharedFxLookup/SharedFxLookupPortableApp_production.runtimeconfig.json create mode 100644 test/HostActivationTests/GivenThatICareAboutMultilevelSharedFxLookup.cs diff --git a/TestAssets/TestProjects/SharedFxLookupPortableApp/Program.cs b/TestAssets/TestProjects/SharedFxLookupPortableApp/Program.cs new file mode 100644 index 0000000000..7c86bcc7fd --- /dev/null +++ b/TestAssets/TestProjects/SharedFxLookupPortableApp/Program.cs @@ -0,0 +1,16 @@ +using System; + +namespace SharedFxLookupPortableApp +{ + public static class Program + { + public static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + Console.WriteLine(string.Join(Environment.NewLine, args)); + + // A small operation involving NewtonSoft.Json to ensure the assembly is loaded properly + var t = typeof(Newtonsoft.Json.JsonReader); + } + } +} diff --git a/TestAssets/TestProjects/SharedFxLookupPortableApp/SharedFxLookupPortableApp.xproj b/TestAssets/TestProjects/SharedFxLookupPortableApp/SharedFxLookupPortableApp.xproj new file mode 100644 index 0000000000..d095392dab --- /dev/null +++ b/TestAssets/TestProjects/SharedFxLookupPortableApp/SharedFxLookupPortableApp.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25123 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + e484fcd8-e438-4559-a745-e58574263abd + PortableApp + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/TestAssets/TestProjects/SharedFxLookupPortableApp/project.json b/TestAssets/TestProjects/SharedFxLookupPortableApp/project.json new file mode 100644 index 0000000000..fd540bb10e --- /dev/null +++ b/TestAssets/TestProjects/SharedFxLookupPortableApp/project.json @@ -0,0 +1,402 @@ +{ + + "buildOptions": { + "emitEntryPoint": true + }, + "dependencies": {}, + "frameworks": { + "netcoreapp2.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "type": "platform", + "version": "2.0.0-beta-*" + }, + "Newtonsoft.Json": "9.0.1-beta1", + "Libuv": { + "version": "1.9.1", + "exclude": "all" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "version": "1.1.0", + "exclude": "all" + }, + "Microsoft.CodeAnalysis.Common": { + "version": "1.3.0", + "exclude": "all" + }, + "Microsoft.CodeAnalysis.CSharp": { + "version": "1.3.0", + "exclude": "all" + }, + "Microsoft.CodeAnalysis.VisualBasic": { + "version": "1.3.0", + "exclude": "all" + }, + "Microsoft.CSharp": { + "version": "4.3.0", + "exclude": "all" + }, + "Microsoft.DiaSymReader.Native": { + "version": "1.4.0", + "exclude": "all" + }, + "Microsoft.VisualBasic": { + "version": "10.1.0", + "exclude": "all" + }, + "Microsoft.Win32.Primitives": { + "version": "4.3.0", + "exclude": "all" + }, + "Microsoft.Win32.Registry": { + "version": "4.3.0", + "exclude": "all" + }, + "System.AppContext": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Buffers": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Collections": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Collections.Concurrent": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Collections.Immutable": { + "version": "1.3.0", + "exclude": "all" + }, + "System.ComponentModel": { + "version": "4.3.0", + "exclude": "all" + }, + "System.ComponentModel.Annotations": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Console": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Diagnostics.Debug": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Diagnostics.DiagnosticSource": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Diagnostics.FileVersionInfo": { + "version": "4.0.0", + "exclude": "all" + }, + "System.Diagnostics.Process": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Diagnostics.StackTrace": { + "version": "4.0.1", + "exclude": "all" + }, + "System.Diagnostics.Tools": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Diagnostics.Tracing": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Dynamic.Runtime": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Globalization": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Globalization.Calendars": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Globalization.Extensions": { + "version": "4.3.0", + "exclude": "all" + }, + "System.IO": { + "version": "4.3.0", + "exclude": "all" + }, + "System.IO.Compression": { + "version": "4.3.0", + "exclude": "all" + }, + "System.IO.Compression.ZipFile": { + "version": "4.3.0", + "exclude": "all" + }, + "System.IO.FileSystem": { + "version": "4.3.0", + "exclude": "all" + }, + "System.IO.FileSystem.Primitives": { + "version": "4.3.0", + "exclude": "all" + }, + "System.IO.FileSystem.Watcher": { + "version": "4.3.0", + "exclude": "all" + }, + "System.IO.MemoryMappedFiles": { + "version": "4.3.0", + "exclude": "all" + }, + "System.IO.UnmanagedMemoryStream": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Linq": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Linq.Expressions": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Linq.Parallel": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Linq.Queryable": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Net.Http": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Net.NameResolution": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Net.Primitives": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Net.Requests": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Net.Security": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Net.Sockets": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Net.WebHeaderCollection": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Numerics.Vectors": { + "version": "4.3.0", + "exclude": "all" + }, + "System.ObjectModel": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Reflection": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Reflection.DispatchProxy": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Reflection.Emit": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Reflection.Emit.ILGeneration": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Reflection.Emit.Lightweight": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Reflection.Extensions": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Reflection.Metadata": { + "version": "1.4.1", + "exclude": "all" + }, + "System.Reflection.Primitives": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Reflection.TypeExtensions": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Resources.Reader": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Resources.ResourceManager": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Runtime": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Runtime.Extensions": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Runtime.Handles": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Runtime.InteropServices": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Runtime.Loader": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Runtime.Numerics": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Security.Claims": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Security.Cryptography.Algorithms": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Security.Cryptography.Cng": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Security.Cryptography.Csp": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Security.Cryptography.Encoding": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Security.Cryptography.OpenSsl": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Security.Cryptography.Primitives": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Security.Cryptography.X509Certificates": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Security.Principal": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Security.Principal.Windows": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Text.Encoding": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Text.Encoding.CodePages": { + "version": "4.0.1", + "exclude": "all" + }, + "System.Text.Encoding.Extensions": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Text.RegularExpressions": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Threading": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Threading.Overlapped": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Threading.Tasks": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Threading.Tasks.Dataflow": { + "version": "4.7.0", + "exclude": "all" + }, + "System.Threading.Tasks.Extensions": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Threading.Tasks.Parallel": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Threading.Thread": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Threading.ThreadPool": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Threading.Timer": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Xml.ReaderWriter": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Xml.XDocument": { + "version": "4.3.0", + "exclude": "all" + }, + "System.Xml.XmlDocument": { + "version": "4.0.1", + "exclude": "all" + }, + "System.Xml.XPath": { + "version": "4.0.1", + "exclude": "all" + }, + "System.Xml.XPath.XDocument": { + "version": "4.0.1", + "exclude": "all" + } + } + } + } +} diff --git a/TestAssets/TestUtils/SharedFxLookup/SharedFxLookupPortableApp_prerelease.runtimeconfig.json b/TestAssets/TestUtils/SharedFxLookup/SharedFxLookupPortableApp_prerelease.runtimeconfig.json new file mode 100644 index 0000000000..6ba8a734f5 --- /dev/null +++ b/TestAssets/TestUtils/SharedFxLookup/SharedFxLookupPortableApp_prerelease.runtimeconfig.json @@ -0,0 +1,8 @@ +{ + "runtimeOptions": { + "framework": { + "name": "Microsoft.NETCore.App", + "version": "9999.0.0-dummy0" + } + } +} \ No newline at end of file diff --git a/TestAssets/TestUtils/SharedFxLookup/SharedFxLookupPortableApp_production.runtimeconfig.json b/TestAssets/TestUtils/SharedFxLookup/SharedFxLookupPortableApp_production.runtimeconfig.json new file mode 100644 index 0000000000..12aa5319e8 --- /dev/null +++ b/TestAssets/TestUtils/SharedFxLookup/SharedFxLookupPortableApp_production.runtimeconfig.json @@ -0,0 +1,8 @@ +{ + "runtimeOptions": { + "framework": { + "name": "Microsoft.NETCore.App", + "version": "9999.0.0" + } + } +} \ No newline at end of file diff --git a/test/HostActivationTests/GivenThatICareAboutMultilevelSharedFxLookup.cs b/test/HostActivationTests/GivenThatICareAboutMultilevelSharedFxLookup.cs new file mode 100644 index 0000000000..f113dcef11 --- /dev/null +++ b/test/HostActivationTests/GivenThatICareAboutMultilevelSharedFxLookup.cs @@ -0,0 +1,421 @@ +using System; +using System.IO; +using Xunit; +using Microsoft.DotNet.InternalAbstractions; + +namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLookup +{ + public class GivenThatICareAboutMultilevelSharedFxLookup + { + private RepoDirectoriesProvider RepoDirectories; + private TestProjectFixture PreviouslyBuiltAndRestoredPortableTestProjectFixture; + + private string _currentWorkingDir; + private string _userDir; + private string _executableDir; + private string _cwdSharedFxBaseDir; + private string _userSharedFxBaseDir; + private string _exeSharedFxBaseDir; + private string _builtSharedFxDir; + private string _cwdSelectedMessage; + private string _userSelectedMessage; + private string _exeSelectedMessage; + private string _sharedFxVersion; + + public GivenThatICareAboutMultilevelSharedFxLookup() + { + // From the artifacts dir, it's possible to find where the sharedFrameworkPublish folder is. We need + // to locate it because we'll copy its contents into other folders + string artifactsDir = Environment.GetEnvironmentVariable("TEST_ARTIFACTS"); + string builtDotnet = Path.Combine(artifactsDir, "..", "..", "intermediate", "sharedFrameworkPublish"); + + // The dotnetMultilevelSharedFxLookup dir will contain some folders and files that will be + // necessary to perform the tests + string baseMultilevelDir = Path.Combine(artifactsDir, "dotnetMultilevelSharedFxLookup"); + string multilevelDir = CalculateMultilevelDirectory(baseMultilevelDir); + + // The three tested locations will be the cwd, the user folder and the exe dir. Both cwd and exe dir + // are easily overwritten, so they will be placed inside the multilevel folder. The actual user location will + // be used during tests + _currentWorkingDir = Path.Combine(multilevelDir, "cwd"); + if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows) + { + _userDir = Environment.GetEnvironmentVariable("USERPROFILE"); + } + else + { + _userDir = Environment.GetEnvironmentVariable("HOME"); + } + _executableDir = Path.Combine(multilevelDir, "exe"); + + // SharedFxBaseDirs contain all available version folders + _cwdSharedFxBaseDir = Path.Combine(_currentWorkingDir, "shared", "Microsoft.NETCore.App"); + _userSharedFxBaseDir = Path.Combine(_userDir, ".dotnet", RuntimeEnvironment.RuntimeArchitecture, "shared", "Microsoft.NETCore.App"); + _exeSharedFxBaseDir = Path.Combine(_executableDir, "shared", "Microsoft.NETCore.App"); + + // Create directories. It's necessary to copy the entire publish folder to the exe dir because + // we'll need to build from it. The CopyDirectory method automatically creates the dest dir + Directory.CreateDirectory(_cwdSharedFxBaseDir); + Directory.CreateDirectory(_userSharedFxBaseDir); + CopyDirectory(builtDotnet, _executableDir); + + // Restore and build SharedFxLookupPortableApp from exe dir + RepoDirectories = new RepoDirectoriesProvider(builtDotnet:_executableDir); + PreviouslyBuiltAndRestoredPortableTestProjectFixture = new TestProjectFixture("SharedFxLookupPortableApp", RepoDirectories) + .EnsureRestored(RepoDirectories.CorehostPackages, RepoDirectories.CorehostDummyPackages) + .BuildProject(); + var fixture = PreviouslyBuiltAndRestoredPortableTestProjectFixture; + + // The actual framework version can be obtained from the built fixture. We'll use it to + // locate the builtSharedFxDir from which we can get the files contained in the version folder + string greatestVersionSharedFxPath = fixture.BuiltDotnet.GreatestVersionSharedFxPath; + _sharedFxVersion = (new DirectoryInfo(greatestVersionSharedFxPath)).Name; + _builtSharedFxDir = Path.Combine(builtDotnet, "shared", "Microsoft.NETCore.App", _sharedFxVersion); + + string hostPolicyDllName = Path.GetFileName(fixture.TestProject.HostPolicyDll); + + // Trace messages used to identify from which folder the framework was picked + _cwdSelectedMessage = $"The expected {hostPolicyDllName} directory is [{_cwdSharedFxBaseDir}"; + _userSelectedMessage = $"The expected {hostPolicyDllName} directory is [{_userSharedFxBaseDir}"; + _exeSelectedMessage = $"The expected {hostPolicyDllName} directory is [{_exeSharedFxBaseDir}"; + } + + [Fact] + public void SharedFxLookup_Must_Verify_Folders_in_the_Correct_Order() + { + var fixture = PreviouslyBuiltAndRestoredPortableTestProjectFixture + .Copy(); + + var dotnet = fixture.BuiltDotnet; + var appDll = fixture.TestProject.AppDll; + + // Set desired version = 9999.0.0 + SetProductionRuntimeConfig(fixture); + + // Add a dummy version in the exe dir + AddAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.0.0"); + + // Version: 9999.0.0 + // CWD: empty + // User: empty + // Exe: 9999.0.0 + // Expected: 9999.0.0 from exe dir + dotnet.Exec(appDll) + .WorkingDirectory(_currentWorkingDir) + .EnvironmentVariable("COREHOST_TRACE", "1") + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should() + .Pass() + .And + .HaveStdErrContaining(_exeSelectedMessage); + + // Add a dummy version in the user dir + AddAvailableSharedFxVersions(_userSharedFxBaseDir, "9999.0.0"); + + // Version: 9999.0.0 + // CWD: empty + // User: 9999.0.0 + // Exe: 9999.0.0 + // Expected: 9999.0.0 from user dir + dotnet.Exec(appDll) + .WorkingDirectory(_currentWorkingDir) + .EnvironmentVariable("COREHOST_TRACE", "1") + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should() + .Pass() + .And + .HaveStdErrContaining(_userSelectedMessage); + + // Add a dummy version in the cwd + AddAvailableSharedFxVersions(_cwdSharedFxBaseDir, "9999.0.0"); + + // Version: 9999.0.0 + // CWD: 9999.0.0 + // User: 9999.0.0 + // Exe: 9999.0.0 + // Expected: 9999.0.0 from cwd + dotnet.Exec(appDll) + .WorkingDirectory(_currentWorkingDir) + .EnvironmentVariable("COREHOST_TRACE", "1") + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should() + .Pass() + .And + .HaveStdErrContaining(_cwdSelectedMessage); + + // Remove dummy folders from user dir + DeleteAvailableSharedFxVersions(_userSharedFxBaseDir, "9999.0.0"); + } + + [Fact] + public void SharedFxLookup_Must_Roll_Forward_Before_Looking_Into_Another_Folder() + { + var fixture = PreviouslyBuiltAndRestoredPortableTestProjectFixture + .Copy(); + + var dotnet = fixture.BuiltDotnet; + var appDll = fixture.TestProject.AppDll; + + // Add some dummy versions + AddAvailableSharedFxVersions(_userSharedFxBaseDir, "9999.0.2", "9999.0.0-dummy2"); + AddAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.0.0", "9999.0.0-dummy0"); + + // Set desired version = 9999.0.0-dummy0 + SetPrereleaseRuntimeConfig(fixture); + + // Version: 9999.0.0-dummy0 + // CWD: empty + // User: 9999.0.2, 9999.0.0-dummy2 + // Exe: 9999.0.0, 9999.0.0-dummy0 + // Expected: 9999.0.0-dummy2 from user dir + dotnet.Exec(appDll) + .WorkingDirectory(_currentWorkingDir) + .EnvironmentVariable("COREHOST_TRACE", "1") + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should() + .Pass() + .And + .HaveStdErrContaining(Path.Combine(_userSelectedMessage, "9999.0.0-dummy2")); + + // Add a prerelease dummy version in CWD + AddAvailableSharedFxVersions(_cwdSharedFxBaseDir, "9999.0.0-dummy1"); + + // Version: 9999.0.0-dummy0 + // CWD: 9999.0.0-dummy1 + // User: 9999.0.2, 9999.0.0-dummy2 + // Exe: 9999.0.0, 9999.0.0-dummy0 + // Expected: 9999.0.0-dummy1 from cwd + dotnet.Exec(appDll) + .WorkingDirectory(_currentWorkingDir) + .EnvironmentVariable("COREHOST_TRACE", "1") + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should() + .Pass() + .And + .HaveStdErrContaining(Path.Combine(_cwdSelectedMessage, "9999.0.0-dummy1")); + + // Set desired version = 9999.0.0 + SetProductionRuntimeConfig(fixture); + + // Version: 9999.0.0 + // CWD: 9999.0.0-dummy1 + // User: 9999.0.2, 9999.0.0-dummy2 + // Exe: 9999.0.0, 9999.0.0-dummy0 + // Expected: 9999.0.2 from user dir + dotnet.Exec(appDll) + .WorkingDirectory(_currentWorkingDir) + .EnvironmentVariable("COREHOST_TRACE", "1") + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should() + .Pass() + .And + .HaveStdErrContaining(Path.Combine(_userSelectedMessage, "9999.0.2")); + + // Add a production dummy version in CWD + AddAvailableSharedFxVersions(_cwdSharedFxBaseDir, "9999.0.1"); + + // Version: 9999.0.0 + // CWD: 9999.0.1, 9999.0.0-dummy1 + // User: 9999.0.2, 9999.0.0-dummy2 + // Exe: 9999.0.0, 9999.0.0-dummy0 + // Expected: 9999.0.1 from cwd + dotnet.Exec(appDll) + .WorkingDirectory(_currentWorkingDir) + .EnvironmentVariable("COREHOST_TRACE", "1") + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should() + .Pass() + .And + .HaveStdErrContaining(Path.Combine(_cwdSelectedMessage, "9999.0.1")); + + // Remove dummy folders from user dir + DeleteAvailableSharedFxVersions(_userSharedFxBaseDir, "9999.0.2", "9999.0.0-dummy2"); + } + + [Fact] + public void SharedFxLookup_Must_Not_Roll_Forward_If_Framework_Version_Is_Specified_Through_Argument() + { + var fixture = PreviouslyBuiltAndRestoredPortableTestProjectFixture + .Copy(); + + var dotnet = fixture.BuiltDotnet; + var appDll = fixture.TestProject.AppDll; + + // Add some dummy versions + AddAvailableSharedFxVersions(_cwdSharedFxBaseDir, "9999.0.1", "9999.0.0-dummy0"); + AddAvailableSharedFxVersions(_userSharedFxBaseDir, "9999.0.2", "9999.0.0-dummy2"); + AddAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.0.0", "9999.0.3", "9999.0.0-dummy3"); + + // Version: 9999.0.0 (through --fx-version arg) + // CWD: 9999.0.1, 9999.0.0-dummy0 + // User: 9999.0.2, 9999.0.0-dummy2 + // Exe: 9999.0.0, 9999.0.3, 9999.0.0-dummy3 + // Expected: 9999.0.0 from exe dir + dotnet.Exec("--fx-version", "9999.0.0", appDll) + .WorkingDirectory(_currentWorkingDir) + .EnvironmentVariable("COREHOST_TRACE", "1") + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should() + .Pass() + .And + .HaveStdErrContaining(Path.Combine(_exeSelectedMessage, "9999.0.0")); + + // Version: 9999.0.0-dummy1 (through --fx-version arg) + // CWD: 9999.0.1, 9999.0.0-dummy0 + // User: 9999.0.2, 9999.0.0-dummy2 + // Exe: 9999.0.0, 9999.0.3, 9999.0.0-dummy3 + // Expected: no compatible version + dotnet.Exec("--fx-version", "9999.0.0-dummy1", appDll) + .WorkingDirectory(_currentWorkingDir) + .EnvironmentVariable("COREHOST_TRACE", "1") + .CaptureStdOut() + .CaptureStdErr() + .Execute(fExpectedToFail:true) + .Should() + .Fail() + .And + .HaveStdErrContaining("It was not possible to find any compatible framework version"); + + // Remove dummy folders from user dir + DeleteAvailableSharedFxVersions(_userSharedFxBaseDir, "9999.0.2", "9999.0.0-dummy2"); + } + + // This method adds a list of new framework version folders in the specified + // sharedFxBaseDir. The files are copied from the _buildSharedFxDir. + // Remarks: + // - If the sharedFxBaseDir does not exist, then a DirectoryNotFoundException + // is thrown. + // - If a specified version folder already exists, then it is deleted and replaced + // with the contents of the _builtSharedFxDir. + private void AddAvailableSharedFxVersions(string sharedFxBaseDir, params string[] availableVersions) + { + DirectoryInfo sharedFxBaseDirInfo = new DirectoryInfo(sharedFxBaseDir); + + if (!sharedFxBaseDirInfo.Exists) + { + throw new DirectoryNotFoundException(); + } + + foreach(string version in availableVersions) + { + string newSharedFxDir = Path.Combine(sharedFxBaseDir, version); + CopyDirectory(_builtSharedFxDir, newSharedFxDir); + } + } + + // This method removes a list of framework version folders from the specified + // sharedFxBaseDir. + // Remarks: + // - If the sharedFxBaseDir does not exist, then a DirectoryNotFoundException + // is thrown. + // - If a specified version folder does not exist, then a DirectoryNotFoundException + // is thrown. + private void DeleteAvailableSharedFxVersions(string sharedFxBaseDir, params string[] availableVersions) + { + DirectoryInfo sharedFxBaseDirInfo = new DirectoryInfo(sharedFxBaseDir); + + if (!sharedFxBaseDirInfo.Exists) + { + throw new DirectoryNotFoundException(); + } + + foreach (string version in availableVersions) + { + string sharedFxDir = Path.Combine(sharedFxBaseDir, version); + if (!Directory.Exists(sharedFxDir)) + { + throw new DirectoryNotFoundException(); + } + Directory.Delete(sharedFxDir, true); + } + } + + // CopyDirectory recursively copies a directory + // Remarks: + // - If the dest dir does not exist, then it is created. + // - If the dest dir exists, then it is substituted with the new one + // (original files and subfolders are deleted). + // - If the src dir does not exist, then a DirectoryNotFoundException + // is thrown. + private void CopyDirectory(string srcDir, string dstDir) + { + DirectoryInfo srcDirInfo = new DirectoryInfo(srcDir); + + if (!srcDirInfo.Exists) + { + throw new DirectoryNotFoundException(); + } + + DirectoryInfo dstDirInfo = new DirectoryInfo(dstDir); + + if (dstDirInfo.Exists) + { + dstDirInfo.Delete(true); + } + + dstDirInfo.Create(); + + foreach (FileInfo fileInfo in srcDirInfo.GetFiles()) + { + string newFile = Path.Combine(dstDir, fileInfo.Name); + fileInfo.CopyTo(newFile); + } + + foreach (DirectoryInfo subdirInfo in srcDirInfo.GetDirectories()) + { + string newDir = Path.Combine(dstDir, subdirInfo.Name); + CopyDirectory(subdirInfo.FullName, newDir); + } + } + + // Overwrites the fixture's runtimeconfig.json. The specified version is 9999.0.0-dummy0 + private void SetPrereleaseRuntimeConfig(TestProjectFixture fixture) + { + string destFile = Path.Combine(fixture.TestProject.OutputDirectory, "SharedFxLookupPortableApp.runtimeconfig.json"); + string srcFile = Path.Combine(RepoDirectories.RepoRoot, "TestAssets", "TestUtils", + "SharedFxLookup", "SharedFxLookupPortableApp_prerelease.runtimeconfig.json"); + File.Copy(srcFile, destFile, true); + } + + // Overwrites the fixture's runtimeconfig.json. The specified version is 9999.0.0 + private void SetProductionRuntimeConfig(TestProjectFixture fixture) + { + string destFile = Path.Combine(fixture.TestProject.OutputDirectory, "SharedFxLookupPortableApp.runtimeconfig.json"); + string srcFile = Path.Combine(RepoDirectories.RepoRoot, "TestAssets", "TestUtils", + "SharedFxLookup", "SharedFxLookupPortableApp_production.runtimeconfig.json"); + File.Copy(srcFile, destFile, true); + } + + // MultilevelDirectory is %TEST_ARTIFACTS%\dotnetMultilevelSharedFxLookup\id. + // We must locate the first non existing id. + private string CalculateMultilevelDirectory(string baseMultilevelDir) + { + int count = 0; + string multilevelDir; + + do + { + multilevelDir = Path.Combine(baseMultilevelDir, count.ToString()); + count++; + } while (Directory.Exists(multilevelDir)); + + return multilevelDir; + } + } +}