From bfa10f1956ee7785af159c9448d46e736cc7a932 Mon Sep 17 00:00:00 2001 From: Swaroop Sridhar Date: Fri, 24 Apr 2020 14:49:21 -0700 Subject: [PATCH] Single-File: Pass BUNDLE_PROBE property to the runtime (#34845) * Single-File: Pass BUNDLE_PROBE property to the runtime As described in the [design doc](https://github.com/dotnet/designs/blob/master/accepted/2020/single-file/design.md#startup), pass the bundle_probe function pointer encoded as a string to the runtime. --- src/installer/corehost/cli/bundle/runner.cpp | 31 +++++-- src/installer/corehost/cli/bundle/runner.h | 3 +- src/installer/corehost/cli/hostmisc/pal.h | 8 +- .../corehost/cli/hostmisc/pal.unix.cpp | 14 ++- .../corehost/cli/hostmisc/pal.windows.cpp | 12 +-- .../corehost/cli/hostpolicy/coreclr.cpp | 3 +- .../corehost/cli/hostpolicy/coreclr.h | 2 +- .../cli/hostpolicy/hostpolicy_context.cpp | 49 ++++++++++ .../BundleProbeTester.csproj | 14 +++ .../TestProjects/BundleProbeTester/Program.cs | 89 +++++++++++++++++++ .../AppHost.Bundle.Tests/BundleProbe.cs | 77 ++++++++++++++++ 11 files changed, 283 insertions(+), 19 deletions(-) create mode 100644 src/installer/test/Assets/TestProjects/BundleProbeTester/BundleProbeTester.csproj create mode 100644 src/installer/test/Assets/TestProjects/BundleProbeTester/Program.cs create mode 100644 src/installer/test/Microsoft.NET.HostModel.Tests/AppHost.Bundle.Tests/BundleProbe.cs diff --git a/src/installer/corehost/cli/bundle/runner.cpp b/src/installer/corehost/cli/bundle/runner.cpp index 58d10a81785..19fc8bcd475 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 fb4d74bf877..39c6b6b1190 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 3b03e748bb1..5c8fa68a278 100644 --- a/src/installer/corehost/cli/hostmisc/pal.h +++ b/src/installer/corehost/cli/hostmisc/pal.h @@ -144,7 +144,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); } @@ -202,6 +204,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); } @@ -235,7 +239,6 @@ namespace pal return ret; } - string_t to_string(int value); string_t get_timestamp(); bool getcwd(string_t* recv); @@ -295,6 +298,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 3626761b0ec..20612a16f6f 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 f8348a05d16..1211c3a6692 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 4acca6a3ea8..a619313c138 100644 --- a/src/installer/corehost/cli/hostpolicy/coreclr.cpp +++ b/src/installer/corehost/cli/hostpolicy/coreclr.cpp @@ -203,7 +203,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 72a48592971..2ad2dbcc789 100644 --- a/src/installer/corehost/cli/hostpolicy/coreclr.h +++ b/src/installer/corehost/cli/hostpolicy/coreclr.h @@ -67,7 +67,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 0ee7bf9cd03..dcaaa179dfe 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) @@ -180,5 +219,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 00000000000..c1141db5ab1 --- /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 00000000000..6a75482b950 --- /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 00000000000..b178e0ee36e --- /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(); + } + } + } +}