diff --git a/src/installer/corehost/cli/bundle/runner.cpp b/src/installer/corehost/cli/bundle/runner.cpp index 58d10a817855f9..19fc8bcd475761 100644 --- a/src/installer/corehost/cli/bundle/runner.cpp +++ b/src/installer/corehost/cli/bundle/runner.cpp @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -48,11 +48,11 @@ StatusCode runner_t::extract() } } -const file_entry_t* runner_t::probe(const pal::string_t& path) const +const file_entry_t* runner_t::probe(const pal::string_t &relative_path) const { for (const file_entry_t& entry : m_manifest.files) { - if (entry.relative_path() == path) + if (pal::pathcmp(entry.relative_path(), relative_path) == 0) { return &entry; } @@ -61,10 +61,28 @@ const file_entry_t* runner_t::probe(const pal::string_t& path) const return nullptr; } +bool runner_t::probe(const pal::string_t& relative_path, int64_t* offset, int64_t* size) const +{ + const bundle::file_entry_t* entry = probe(relative_path); + + if (entry == nullptr) + { + return false; + } + + assert(entry->offset() != 0); + + *offset = entry->offset(); + *size = entry->size(); + + + return true; +} + + bool runner_t::locate(const pal::string_t& relative_path, pal::string_t& full_path) const { - const bundle::runner_t* app = bundle::runner_t::app(); - const bundle::file_entry_t* entry = app->probe(relative_path); + const bundle::file_entry_t* entry = probe(relative_path); if (entry == nullptr) { @@ -76,8 +94,9 @@ bool runner_t::locate(const pal::string_t& relative_path, pal::string_t& full_pa // The json files are not queried by the host using this method. assert(entry->needs_extraction()); - full_path.assign(app->extraction_path()); + full_path.assign(extraction_path()); append_path(&full_path, relative_path.c_str()); return true; } + diff --git a/src/installer/corehost/cli/bundle/runner.h b/src/installer/corehost/cli/bundle/runner.h index fb4d74bf877a2f..39c6b6b1190b01 100644 --- a/src/installer/corehost/cli/bundle/runner.h +++ b/src/installer/corehost/cli/bundle/runner.h @@ -27,7 +27,7 @@ namespace bundle const pal::string_t& extraction_path() const { return m_extraction_path; } - const file_entry_t *probe(const pal::string_t& path) const; + bool probe(const pal::string_t& relative_path, int64_t* offset, int64_t* size) const; bool locate(const pal::string_t& relative_path, pal::string_t& full_path) const; static StatusCode process_manifest_and_extract() @@ -40,6 +40,7 @@ namespace bundle private: StatusCode extract(); + const file_entry_t* probe(const pal::string_t& relative_path) const; manifest_t m_manifest; pal::string_t m_extraction_path; diff --git a/src/installer/corehost/cli/hostmisc/pal.h b/src/installer/corehost/cli/hostmisc/pal.h index 10cb0e2c302ae7..fefe5c21bd4463 100644 --- a/src/installer/corehost/cli/hostmisc/pal.h +++ b/src/installer/corehost/cli/hostmisc/pal.h @@ -146,7 +146,9 @@ namespace pal inline int strcasecmp(const char_t* str1, const char_t* str2) { return ::_wcsicmp(str1, str2); } inline int strncmp(const char_t* str1, const char_t* str2, int len) { return ::wcsncmp(str1, str2, len); } inline int strncasecmp(const char_t* str1, const char_t* str2, int len) { return ::_wcsnicmp(str1, str2, len); } - + inline int pathcmp(const pal::string_t &path1, const pal::string_t &path2) { return strcasecmp(path1.c_str(), path2.c_str()); } + inline string_t to_string(int value) { return std::to_wstring(value); } + inline size_t strlen(const char_t* str) { return ::wcslen(str); } inline FILE * file_open(const string_t& path, const char_t* mode) { return ::_wfopen(path.c_str(), mode); } @@ -204,6 +206,8 @@ namespace pal inline int strcasecmp(const char_t* str1, const char_t* str2) { return ::strcasecmp(str1, str2); } inline int strncmp(const char_t* str1, const char_t* str2, int len) { return ::strncmp(str1, str2, len); } inline int strncasecmp(const char_t* str1, const char_t* str2, int len) { return ::strncasecmp(str1, str2, len); } + inline int pathcmp(const pal::string_t& path1, const pal::string_t& path2) { return strcmp(path1.c_str(), path2.c_str()); } + inline string_t to_string(int value) { return std::to_string(value); } inline size_t strlen(const char_t* str) { return ::strlen(str); } inline FILE * file_open(const string_t& path, const char_t* mode) { return fopen(path.c_str(), mode); } @@ -237,7 +241,6 @@ namespace pal return ret; } - string_t to_string(int value); string_t get_timestamp(); bool getcwd(string_t* recv); @@ -297,6 +300,7 @@ namespace pal bool get_default_bundle_extraction_base_dir(string_t& extraction_dir); int xtoi(const char_t* input); + bool unicode_palstring(const char16_t* str, pal::string_t* out); bool get_loaded_library(const char_t *library_name, const char *symbol_name, /*out*/ dll_t *dll, /*out*/ string_t *path); bool load_library(const string_t* path, dll_t* dll); diff --git a/src/installer/corehost/cli/hostmisc/pal.unix.cpp b/src/installer/corehost/cli/hostmisc/pal.unix.cpp index 3626761b0ecbb4..20612a16f6fcfe 100644 --- a/src/installer/corehost/cli/hostmisc/pal.unix.cpp +++ b/src/installer/corehost/cli/hostmisc/pal.unix.cpp @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include #include "config.h" @@ -39,8 +41,6 @@ #define DT_LNK 10 #endif -pal::string_t pal::to_string(int value) { return std::to_string(value); } - pal::string_t pal::to_lower(const pal::string_t& in) { pal::string_t ret = in; @@ -254,6 +254,16 @@ int pal::xtoi(const char_t* input) return atoi(input); } +bool pal::unicode_palstring(const char16_t* str, pal::string_t* out) +{ + out->clear(); + + std::wstring_convert, char16_t> conversion; + out->assign(conversion.to_bytes(str)); + + return true; +} + bool pal::is_path_rooted(const pal::string_t& path) { return path.front() == '/'; diff --git a/src/installer/corehost/cli/hostmisc/pal.windows.cpp b/src/installer/corehost/cli/hostmisc/pal.windows.cpp index f8348a05d167b0..1211c3a6692b48 100644 --- a/src/installer/corehost/cli/hostmisc/pal.windows.cpp +++ b/src/installer/corehost/cli/hostmisc/pal.windows.cpp @@ -49,11 +49,6 @@ pal::string_t pal::to_lower(const pal::string_t& in) return ret; } -pal::string_t pal::to_string(int value) -{ - return std::to_wstring(value); -} - pal::string_t pal::get_timestamp() { std::time_t t = std::time(0); @@ -605,7 +600,6 @@ bool pal::get_default_bundle_extraction_base_dir(pal::string_t& extraction_dir) return realpath(&extraction_dir); } - static bool wchar_convert_helper(DWORD code_page, const char* cstr, int len, pal::string_t* out) { out->clear(); @@ -649,6 +643,12 @@ bool pal::clr_palstring(const char* cstr, pal::string_t* out) return wchar_convert_helper(CP_UTF8, cstr, ::strlen(cstr), out); } +bool pal::unicode_palstring(const char16_t* str, pal::string_t* out) +{ + out->assign((const wchar_t *)str); + return true; +} + // Return if path is valid and file exists, return true and adjust path as appropriate. bool pal::realpath(string_t* path, bool skip_error_logging) { diff --git a/src/installer/corehost/cli/hostpolicy/coreclr.cpp b/src/installer/corehost/cli/hostpolicy/coreclr.cpp index b329a72d85886a..8c513d0b857ecc 100644 --- a/src/installer/corehost/cli/hostpolicy/coreclr.cpp +++ b/src/installer/corehost/cli/hostpolicy/coreclr.cpp @@ -204,7 +204,8 @@ namespace _X("STARTUP_HOOKS"), _X("APP_PATHS"), _X("APP_NI_PATHS"), - _X("RUNTIME_IDENTIFIER") + _X("RUNTIME_IDENTIFIER"), + _X("BUNDLE_PROBE") }; static_assert((sizeof(PropertyNameMapping) / sizeof(*PropertyNameMapping)) == static_cast(common_property::Last), "Invalid property count"); diff --git a/src/installer/corehost/cli/hostpolicy/coreclr.h b/src/installer/corehost/cli/hostpolicy/coreclr.h index a06e81fe2e346f..422b57c52d59d0 100644 --- a/src/installer/corehost/cli/hostpolicy/coreclr.h +++ b/src/installer/corehost/cli/hostpolicy/coreclr.h @@ -68,7 +68,7 @@ enum class common_property AppPaths, AppNIPaths, RuntimeIdentifier, - + BundleProbe, // Sentinel value - new values should be defined above Last }; diff --git a/src/installer/corehost/cli/hostpolicy/hostpolicy_context.cpp b/src/installer/corehost/cli/hostpolicy/hostpolicy_context.cpp index bb4a965126d962..c78c51493a1ef4 100644 --- a/src/installer/corehost/cli/hostpolicy/hostpolicy_context.cpp +++ b/src/installer/corehost/cli/hostpolicy/hostpolicy_context.cpp @@ -7,6 +7,8 @@ #include "deps_resolver.h" #include #include +#include "bundle/runner.h" +#include "bundle/file_entry.h" namespace { @@ -15,6 +17,43 @@ namespace trace::error(_X("Duplicate runtime property found: %s"), property_key); trace::error(_X("It is invalid to specify values for properties populated by the hosting layer in the the application's .runtimeconfig.json")); } + + // bundle_probe: + // Probe the app-bundle for the file 'path' and return its location ('offset', 'size') if found. + // + // This function is an API exported to the runtime via the BUNDLE_PROBE property. + // This function used by the runtime to probe for bundled assemblies + // This function assumes that the currently executing app is a single-file bundle. + // + // bundle_probe recieves its path argument as cha16_t* instead of pal::char_t*, because: + // * The host uses Unicode strings on Windows and UTF8 strings on Unix + // * The runtime uses Unicode strings on all platforms + // * Using a unicode encoded path presents a uniform interface to the runtime + // and minimizes the number if Unicode <-> UTF8 conversions necessary. + // + // The unicode char type is char16_t* instead of whcar_t*, because: + // * wchar_t is 16-bit encoding on Windows while it is 32-bit encoding on most Unix systems + // * The runtime uses 16-bit encoded unicode characters. + + bool STDMETHODCALLTYPE bundle_probe(const char16_t* path, int64_t* offset, int64_t* size) + { + if (path == nullptr) + { + return false; + } + + pal::string_t file_path; + + if (!pal::unicode_palstring(path, &file_path)) + { + trace::warning(_X("Failure probing contents of the application bundle.")); + trace::warning(_X("Failed to convert path [%ls] to UTF8"), path); + + return false; + } + + return bundle::runner_t::app()->probe(file_path, offset, size); + } } int hostpolicy_context_t::initialize(hostpolicy_init_t &hostpolicy_init, const arguments_t &args, bool enable_breadcrumbs) @@ -198,5 +237,15 @@ int hostpolicy_context_t::initialize(hostpolicy_init_t &hostpolicy_init, const a } } + // Single-File Bundle Probe + if (bundle::info_t::is_single_file_bundle()) + { + // Encode the bundle_probe function pointer as a string, and pass it to the runtime. + pal::stringstream_t ptr_stream; + ptr_stream << "0x" << std::hex << (size_t)(&bundle_probe); + + coreclr_properties.add(common_property::BundleProbe, ptr_stream.str().c_str()); + } + return StatusCode::Success; } diff --git a/src/installer/test/Assets/TestProjects/BundleProbeTester/BundleProbeTester.csproj b/src/installer/test/Assets/TestProjects/BundleProbeTester/BundleProbeTester.csproj new file mode 100644 index 00000000000000..c1141db5ab1ad4 --- /dev/null +++ b/src/installer/test/Assets/TestProjects/BundleProbeTester/BundleProbeTester.csproj @@ -0,0 +1,14 @@ + + + + $(NETCoreAppFramework) + Exe + $(TestTargetRid) + $(MNAVersion) + + + + true + + + diff --git a/src/installer/test/Assets/TestProjects/BundleProbeTester/Program.cs b/src/installer/test/Assets/TestProjects/BundleProbeTester/Program.cs new file mode 100644 index 00000000000000..6a75482b95071f --- /dev/null +++ b/src/installer/test/Assets/TestProjects/BundleProbeTester/Program.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace BundleProbeTester +{ + public static class Program + { + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate bool BundleProbeDelegate([MarshalAs(UnmanagedType.LPWStr)] string path, IntPtr size, IntPtr offset); + + unsafe static bool Probe(BundleProbeDelegate bundleProbe, string path, bool isExpected) + { + Int64 size, offset; + bool exists = bundleProbe(path, (IntPtr)(&offset), (IntPtr)(&size)); + + switch (exists, isExpected) + { + case (true, true): + if (size > 0 && offset > 0) + { + return true; + } + + Console.WriteLine($"Invalid location obtained for {path} within bundle."); + return false; + + case (true, false): + Console.WriteLine($"Unexpected file {path} found in bundle."); + return false; + + case (false, true): + Console.WriteLine($"Expected file {path} not found in bundle."); + return false; + + case (false, false): + return true; + } + + return false; // dummy + } + + public static int Main(string[] args) + { + bool isSingleFile = args.Length > 0 && args[0].Equals("SingleFile"); + object probeObject = System.AppDomain.CurrentDomain.GetData("BUNDLE_PROBE"); + + if (!isSingleFile) + { + if (probeObject != null) + { + Console.WriteLine("BUNDLE_PROBE property passed in for a non-single-file app"); + return -1; + } + + Console.WriteLine("No BUNDLE_PROBE"); + return 0; + } + + if (probeObject == null) + { + Console.WriteLine("BUNDLE_PROBE property not passed in for a single-file app"); + return -2; + } + + string probeString = probeObject as string; + IntPtr probePtr = (IntPtr)Convert.ToUInt64(probeString, 16); + BundleProbeDelegate bundleProbeDelegate = Marshal.GetDelegateForFunctionPointer(probePtr); + bool success = + Probe(bundleProbeDelegate, "BundleProbeTester.dll", isExpected: true) && + Probe(bundleProbeDelegate, "BundleProbeTester.runtimeconfig.json", isExpected: true) && + Probe(bundleProbeDelegate, "System.Private.CoreLib.dll", isExpected: true) && + Probe(bundleProbeDelegate, "hostpolicy.dll", isExpected: false) && + Probe(bundleProbeDelegate, "--", isExpected: false) && + Probe(bundleProbeDelegate, "", isExpected: false); + + if (!success) + { + return -3; + } + + Console.WriteLine("BUNDLE_PROBE OK"); + return 0; + } + } +} diff --git a/src/installer/test/Microsoft.NET.HostModel.Tests/AppHost.Bundle.Tests/BundleProbe.cs b/src/installer/test/Microsoft.NET.HostModel.Tests/AppHost.Bundle.Tests/BundleProbe.cs new file mode 100644 index 00000000000000..b178e0ee36e6d0 --- /dev/null +++ b/src/installer/test/Microsoft.NET.HostModel.Tests/AppHost.Bundle.Tests/BundleProbe.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Xunit; +using Microsoft.DotNet.Cli.Build.Framework; +using Microsoft.DotNet.CoreSetup.Test; +using BundleTests.Helpers; +using System.Threading; + +namespace AppHost.Bundle.Tests +{ + public class BundleProbe : IClassFixture + { + private SharedTestState sharedTestState; + + public BundleProbe(SharedTestState fixture) + { + sharedTestState = fixture; + } + + [Fact] + private void Bundle_Probe_Not_Passed_For_Non_Single_File_App() + { + var fixture = sharedTestState.TestFixture.Copy(); + string appExe = BundleHelper.GetHostPath(fixture); + + Command.Create(appExe) + .CaptureStdErr() + .CaptureStdOut() + .Execute() + .Should() + .Pass() + .And + .HaveStdOutContaining("No BUNDLE_PROBE"); + } + + [Fact] + private void Bundle_Probe_Passed_For_Single_File_App() + { + var fixture = sharedTestState.TestFixture.Copy(); + string singleFile = BundleHelper.BundleApp(fixture); + + Command.Create(singleFile, "SingleFile") + .CaptureStdErr() + .CaptureStdOut() + .Execute() + .Should() + .Pass() + .And + .HaveStdOutContaining("BUNDLE_PROBE OK"); + } + + public class SharedTestState : IDisposable + { + public TestProjectFixture TestFixture { get; set; } + public RepoDirectoriesProvider RepoDirectories { get; set; } + + public SharedTestState() + { + RepoDirectories = new RepoDirectoriesProvider(); + TestFixture = new TestProjectFixture("BundleProbeTester", RepoDirectories); + TestFixture + .EnsureRestoredForRid(TestFixture.CurrentRid, RepoDirectories.CorehostPackages) + .PublishProject(runtime: TestFixture.CurrentRid, + outputDirectory: BundleHelper.GetPublishPath(TestFixture)); + } + + public void Dispose() + { + TestFixture.Dispose(); + } + } + } +}