Single-File: Pass BUNDLE_PROBE property to the runtime (#34845)
authorSwaroop Sridhar <Swaroop.Sridhar@microsoft.com>
Fri, 24 Apr 2020 21:49:21 +0000 (14:49 -0700)
committerGitHub <noreply@github.com>
Fri, 24 Apr 2020 21:49:21 +0000 (14:49 -0700)
* 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
src/installer/corehost/cli/bundle/runner.h
src/installer/corehost/cli/hostmisc/pal.h
src/installer/corehost/cli/hostmisc/pal.unix.cpp
src/installer/corehost/cli/hostmisc/pal.windows.cpp
src/installer/corehost/cli/hostpolicy/coreclr.cpp
src/installer/corehost/cli/hostpolicy/coreclr.h
src/installer/corehost/cli/hostpolicy/hostpolicy_context.cpp
src/installer/test/Assets/TestProjects/BundleProbeTester/BundleProbeTester.csproj [new file with mode: 0644]
src/installer/test/Assets/TestProjects/BundleProbeTester/Program.cs [new file with mode: 0644]
src/installer/test/Microsoft.NET.HostModel.Tests/AppHost.Bundle.Tests/BundleProbe.cs [new file with mode: 0644]

index 58d10a8..19fc8bc 100644 (file)
@@ -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_tpath) 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;
 }
+
index fb4d74b..39c6b6b 100644 (file)
@@ -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;
index 3b03e74..5c8fa68 100644 (file)
@@ -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);
index 3626761..20612a1 100644 (file)
@@ -17,6 +17,8 @@
 #include <fcntl.h>
 #include <fnmatch.h>
 #include <ctime>
+#include <locale>
+#include <codecvt>
 #include <pwd.h>
 #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<std::codecvt_utf8_utf16<char16_t>, char16_t> conversion;
+    out->assign(conversion.to_bytes(str));
+
+    return true;
+}
+
 bool pal::is_path_rooted(const pal::string_t& path)
 {
     return path.front() == '/';
index f8348a0..1211c3a 100644 (file)
@@ -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)
 {
index 4acca6a..a619313 100644 (file)
@@ -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<size_t>(common_property::Last), "Invalid property count");
index 72a4859..2ad2dbc 100644 (file)
@@ -67,7 +67,7 @@ enum class common_property
     AppPaths,
     AppNIPaths,
     RuntimeIdentifier,
-
+    BundleProbe,
     // Sentinel value - new values should be defined above
     Last
 };
index 0ee7bf9..dcaaa17 100644 (file)
@@ -7,6 +7,8 @@
 #include "deps_resolver.h"
 #include <error_codes.h>
 #include <trace.h>
+#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 (file)
index 0000000..c1141db
--- /dev/null
@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>$(NETCoreAppFramework)</TargetFramework>
+    <OutputType>Exe</OutputType>
+    <RuntimeIdentifier>$(TestTargetRid)</RuntimeIdentifier>
+    <RuntimeFrameworkVersion>$(MNAVersion)</RuntimeFrameworkVersion>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+  </PropertyGroup>
+
+</Project>
diff --git a/src/installer/test/Assets/TestProjects/BundleProbeTester/Program.cs b/src/installer/test/Assets/TestProjects/BundleProbeTester/Program.cs
new file mode 100644 (file)
index 0000000..6a75482
--- /dev/null
@@ -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<BundleProbeDelegate>(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 (file)
index 0000000..b178e0e
--- /dev/null
@@ -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<BundleProbe.SharedTestState>
+    {
+        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();
+            }
+        }
+    }
+}