mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Remove bundled HostModel dll and include runtime code
This commit is contained in:
92
src/Microsoft.NET.HostModel/AppHost/AppHostExceptions.cs
Normal file
92
src/Microsoft.NET.HostModel/AppHost/AppHostExceptions.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.NET.HostModel.AppHost
|
||||
{
|
||||
/// <summary>
|
||||
/// An instance of this exception is thrown when an AppHost binary update
|
||||
/// fails due to known user errors.
|
||||
/// </summary>
|
||||
public class AppHostUpdateException : Exception
|
||||
{
|
||||
internal AppHostUpdateException(string message = null)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The application host executable cannot be customized because adding resources requires
|
||||
/// that the build be performed on Windows (excluding Nano Server).
|
||||
/// </summary>
|
||||
public sealed class AppHostCustomizationUnsupportedOSException : AppHostUpdateException
|
||||
{
|
||||
internal AppHostCustomizationUnsupportedOSException()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The MachO application host executable cannot be customized because
|
||||
/// it was not in the expected format
|
||||
/// </summary>
|
||||
public sealed class AppHostMachOFormatException : AppHostUpdateException
|
||||
{
|
||||
public readonly MachOFormatError Error;
|
||||
|
||||
internal AppHostMachOFormatException(MachOFormatError error)
|
||||
{
|
||||
Error = error;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unable to use the input file as application host executable because it's not a
|
||||
/// Windows executable for the CUI (Console) subsystem.
|
||||
/// </summary>
|
||||
public sealed class AppHostNotCUIException : AppHostUpdateException
|
||||
{
|
||||
internal AppHostNotCUIException()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unable to use the input file as an application host executable
|
||||
/// because it's not a Windows PE file
|
||||
/// </summary>
|
||||
public sealed class AppHostNotPEFileException : AppHostUpdateException
|
||||
{
|
||||
internal AppHostNotPEFileException()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unable to sign the apphost binary.
|
||||
/// </summary>
|
||||
public sealed class AppHostSigningException : AppHostUpdateException
|
||||
{
|
||||
public readonly int ExitCode;
|
||||
|
||||
internal AppHostSigningException(int exitCode, string signingErrorMessage)
|
||||
: base(signingErrorMessage)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given app file name is longer than 1024 bytes
|
||||
/// </summary>
|
||||
public sealed class AppNameTooLongException : AppHostUpdateException
|
||||
{
|
||||
public string LongName { get; }
|
||||
|
||||
internal AppNameTooLongException(string name)
|
||||
{
|
||||
LongName = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
202
src/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs
Normal file
202
src/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
|
||||
namespace Microsoft.NET.HostModel.AppHost
|
||||
{
|
||||
public static class BinaryUtils
|
||||
{
|
||||
internal static unsafe void SearchAndReplace(
|
||||
MemoryMappedViewAccessor accessor,
|
||||
byte[] searchPattern,
|
||||
byte[] patternToReplace,
|
||||
bool pad0s = true)
|
||||
{
|
||||
byte* pointer = null;
|
||||
|
||||
try
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
|
||||
byte* bytes = pointer + accessor.PointerOffset;
|
||||
|
||||
int position = KMPSearch(searchPattern, bytes, accessor.Capacity);
|
||||
if (position < 0)
|
||||
{
|
||||
throw new PlaceHolderNotFoundInAppHostException(searchPattern);
|
||||
}
|
||||
|
||||
accessor.WriteArray(
|
||||
position: position,
|
||||
array: patternToReplace,
|
||||
offset: 0,
|
||||
count: patternToReplace.Length);
|
||||
|
||||
if (pad0s)
|
||||
{
|
||||
Pad0(searchPattern, patternToReplace, bytes, position);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pointer != null)
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void Pad0(byte[] searchPattern, byte[] patternToReplace, byte* bytes, int offset)
|
||||
{
|
||||
if (patternToReplace.Length < searchPattern.Length)
|
||||
{
|
||||
for (int i = patternToReplace.Length; i < searchPattern.Length; i++)
|
||||
{
|
||||
bytes[i + offset] = 0x0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe void SearchAndReplace(
|
||||
string filePath,
|
||||
byte[] searchPattern,
|
||||
byte[] patternToReplace,
|
||||
bool pad0s = true)
|
||||
{
|
||||
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
|
||||
{
|
||||
using (var accessor = mappedFile.CreateViewAccessor())
|
||||
{
|
||||
SearchAndReplace(accessor, searchPattern, patternToReplace, pad0s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static unsafe int SearchInFile(MemoryMappedViewAccessor accessor, byte[] searchPattern)
|
||||
{
|
||||
var safeBuffer = accessor.SafeMemoryMappedViewHandle;
|
||||
return KMPSearch(searchPattern, (byte*)safeBuffer.DangerousGetHandle(), (int)safeBuffer.ByteLength);
|
||||
}
|
||||
|
||||
public static unsafe int SearchInFile(string filePath, byte[] searchPattern)
|
||||
{
|
||||
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
|
||||
{
|
||||
using (var accessor = mappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read))
|
||||
{
|
||||
return SearchInFile(accessor, searchPattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm
|
||||
private static int[] ComputeKMPFailureFunction(byte[] pattern)
|
||||
{
|
||||
int[] table = new int[pattern.Length];
|
||||
if (pattern.Length >= 1)
|
||||
{
|
||||
table[0] = -1;
|
||||
}
|
||||
if (pattern.Length >= 2)
|
||||
{
|
||||
table[1] = 0;
|
||||
}
|
||||
|
||||
int pos = 2;
|
||||
int cnd = 0;
|
||||
while (pos < pattern.Length)
|
||||
{
|
||||
if (pattern[pos - 1] == pattern[cnd])
|
||||
{
|
||||
table[pos] = cnd + 1;
|
||||
cnd++;
|
||||
pos++;
|
||||
}
|
||||
else if (cnd > 0)
|
||||
{
|
||||
cnd = table[cnd];
|
||||
}
|
||||
else
|
||||
{
|
||||
table[pos] = 0;
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
// See: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm
|
||||
private static unsafe int KMPSearch(byte[] pattern, byte* bytes, long bytesLength)
|
||||
{
|
||||
int m = 0;
|
||||
int i = 0;
|
||||
int[] table = ComputeKMPFailureFunction(pattern);
|
||||
|
||||
while (m + i < bytesLength)
|
||||
{
|
||||
if (pattern[i] == bytes[m + i])
|
||||
{
|
||||
if (i == pattern.Length - 1)
|
||||
{
|
||||
return m;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (table[i] > -1)
|
||||
{
|
||||
m = m + i - table[i];
|
||||
i = table[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
m++;
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static void CopyFile(string sourcePath, string destinationPath)
|
||||
{
|
||||
var destinationDirectory = new FileInfo(destinationPath).Directory.FullName;
|
||||
if (!Directory.Exists(destinationDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(destinationDirectory);
|
||||
}
|
||||
|
||||
// Copy file to destination path so it inherits the same attributes/permissions.
|
||||
File.Copy(sourcePath, destinationPath, overwrite: true);
|
||||
}
|
||||
|
||||
internal static void WriteToStream(MemoryMappedViewAccessor sourceViewAccessor, FileStream fileStream, long length)
|
||||
{
|
||||
int pos = 0;
|
||||
int bufSize = 16384; //16K
|
||||
|
||||
byte[] buf = new byte[bufSize];
|
||||
length = Math.Min(length, sourceViewAccessor.Capacity);
|
||||
do
|
||||
{
|
||||
int bytesRequested = Math.Min((int)length - pos, bufSize);
|
||||
if (bytesRequested <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
int bytesRead = sourceViewAccessor.ReadArray(pos, buf, 0, bytesRequested);
|
||||
if (bytesRead > 0)
|
||||
{
|
||||
fileStream.Write(buf, 0, bytesRead);
|
||||
pos += bytesRead;
|
||||
}
|
||||
}
|
||||
while (true);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/Microsoft.NET.HostModel/AppHost/ElfUtils.cs
Normal file
51
src/Microsoft.NET.HostModel/AppHost/ElfUtils.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.NET.HostModel.AppHost
|
||||
{
|
||||
internal static class ElfUtils
|
||||
{
|
||||
// The Linux Headers are copied from elf.h
|
||||
|
||||
#pragma warning disable 0649
|
||||
private struct ElfHeader
|
||||
{
|
||||
private byte EI_MAG0;
|
||||
private byte EI_MAG1;
|
||||
private byte EI_MAG2;
|
||||
private byte EI_MAG3;
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return EI_MAG0 == 0x7f &&
|
||||
EI_MAG1 == 0x45 &&
|
||||
EI_MAG2 == 0x4C &&
|
||||
EI_MAG3 == 0x46;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
#pragma warning restore 0649
|
||||
|
||||
public static bool IsElfImage(string filePath)
|
||||
{
|
||||
using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
|
||||
{
|
||||
if (reader.BaseStream.Length < 16) // EI_NIDENT = 16
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] eIdent = reader.ReadBytes(4);
|
||||
|
||||
// Check that the first four bytes are 0x7f, 'E', 'L', 'F'
|
||||
return eIdent[0] == 0x7f &&
|
||||
eIdent[1] == 0x45 &&
|
||||
eIdent[2] == 0x4C &&
|
||||
eIdent[3] == 0x46;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/Microsoft.NET.HostModel/AppHost/HResultException.cs
Normal file
22
src/Microsoft.NET.HostModel/AppHost/HResultException.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.NET.HostModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an exception thrown because of a Win32 error
|
||||
/// </summary>
|
||||
public class HResultException : Exception
|
||||
{
|
||||
public readonly int Win32HResult;
|
||||
public HResultException(int hResult) : base(hResult.ToString("X4"))
|
||||
{
|
||||
Win32HResult = hResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
267
src/Microsoft.NET.HostModel/AppHost/HostWriter.cs
Normal file
267
src/Microsoft.NET.HostModel/AppHost/HostWriter.cs
Normal file
@@ -0,0 +1,267 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.NET.HostModel.AppHost
|
||||
{
|
||||
/// <summary>
|
||||
/// Embeds the App Name into the AppHost.exe
|
||||
/// If an apphost is a single-file bundle, updates the location of the bundle headers.
|
||||
/// </summary>
|
||||
public static class HostWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// hash value embedded in default apphost executable in a place where the path to the app binary should be stored.
|
||||
/// </summary>
|
||||
private const string AppBinaryPathPlaceholder = "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2";
|
||||
private static readonly byte[] AppBinaryPathPlaceholderSearchValue = Encoding.UTF8.GetBytes(AppBinaryPathPlaceholder);
|
||||
|
||||
/// <summary>
|
||||
/// Create an AppHost with embedded configuration of app binary location
|
||||
/// </summary>
|
||||
/// <param name="appHostSourceFilePath">The path of Apphost template, which has the place holder</param>
|
||||
/// <param name="appHostDestinationFilePath">The destination path for desired location to place, including the file name</param>
|
||||
/// <param name="appBinaryFilePath">Full path to app binary or relative path to the result apphost file</param>
|
||||
/// <param name="windowsGraphicalUserInterface">Specify whether to set the subsystem to GUI. Only valid for PE apphosts.</param>
|
||||
/// <param name="assemblyToCopyResorcesFrom">Path to the intermediate assembly, used for copying resources to PE apphosts.</param>
|
||||
/// <param name="enableMacOSCodeSign">Sign the app binary using codesign with an anonymous certificate.</param>
|
||||
public static void CreateAppHost(
|
||||
string appHostSourceFilePath,
|
||||
string appHostDestinationFilePath,
|
||||
string appBinaryFilePath,
|
||||
bool windowsGraphicalUserInterface = false,
|
||||
string assemblyToCopyResorcesFrom = null,
|
||||
bool enableMacOSCodeSign = false)
|
||||
{
|
||||
var bytesToWrite = Encoding.UTF8.GetBytes(appBinaryFilePath);
|
||||
if (bytesToWrite.Length > 1024)
|
||||
{
|
||||
throw new AppNameTooLongException(appBinaryFilePath);
|
||||
}
|
||||
|
||||
bool appHostIsPEImage = false;
|
||||
|
||||
void RewriteAppHost(MemoryMappedViewAccessor accessor)
|
||||
{
|
||||
// Re-write the destination apphost with the proper contents.
|
||||
BinaryUtils.SearchAndReplace(accessor, AppBinaryPathPlaceholderSearchValue, bytesToWrite);
|
||||
|
||||
appHostIsPEImage = PEUtils.IsPEImage(accessor);
|
||||
|
||||
if (windowsGraphicalUserInterface)
|
||||
{
|
||||
if (!appHostIsPEImage)
|
||||
{
|
||||
throw new AppHostNotPEFileException();
|
||||
}
|
||||
|
||||
PEUtils.SetWindowsGraphicalUserInterfaceBit(accessor);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateResources()
|
||||
{
|
||||
if (assemblyToCopyResorcesFrom != null && appHostIsPEImage)
|
||||
{
|
||||
if (ResourceUpdater.IsSupportedOS())
|
||||
{
|
||||
// Copy resources from managed dll to the apphost
|
||||
new ResourceUpdater(appHostDestinationFilePath)
|
||||
.AddResourcesFromPEImage(assemblyToCopyResorcesFrom)
|
||||
.Update();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new AppHostCustomizationUnsupportedOSException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RetryUtil.RetryOnIOError(() =>
|
||||
{
|
||||
FileStream appHostSourceStream = null;
|
||||
MemoryMappedFile memoryMappedFile = null;
|
||||
MemoryMappedViewAccessor memoryMappedViewAccessor = null;
|
||||
try
|
||||
{
|
||||
// Open the source host file.
|
||||
appHostSourceStream = new FileStream(appHostSourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostSourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true);
|
||||
memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.CopyOnWrite);
|
||||
|
||||
// Get the size of the source app host to ensure that we don't write extra data to the destination.
|
||||
// On Windows, the size of the view accessor is rounded up to the next page boundary.
|
||||
long sourceAppHostLength = appHostSourceStream.Length;
|
||||
|
||||
// Transform the host file in-memory.
|
||||
RewriteAppHost(memoryMappedViewAccessor);
|
||||
|
||||
// Save the transformed host.
|
||||
using (FileStream fileStream = new FileStream(appHostDestinationFilePath, FileMode.Create))
|
||||
{
|
||||
BinaryUtils.WriteToStream(memoryMappedViewAccessor, fileStream, sourceAppHostLength);
|
||||
|
||||
// Remove the signature from MachO hosts.
|
||||
if (!appHostIsPEImage)
|
||||
{
|
||||
MachOUtils.RemoveSignature(fileStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
memoryMappedViewAccessor?.Dispose();
|
||||
memoryMappedFile?.Dispose();
|
||||
appHostSourceStream?.Dispose();
|
||||
}
|
||||
});
|
||||
|
||||
RetryUtil.RetryOnWin32Error(UpdateResources);
|
||||
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
var filePermissionOctal = Convert.ToInt32("755", 8); // -rwxr-xr-x
|
||||
const int EINTR = 4;
|
||||
int chmodReturnCode = 0;
|
||||
|
||||
do
|
||||
{
|
||||
chmodReturnCode = chmod(appHostDestinationFilePath, filePermissionOctal);
|
||||
}
|
||||
while (chmodReturnCode == -1 && Marshal.GetLastWin32Error() == EINTR);
|
||||
|
||||
if (chmodReturnCode == -1)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not set file permission {filePermissionOctal} for {appHostDestinationFilePath}.");
|
||||
}
|
||||
|
||||
if (enableMacOSCodeSign && RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
CodeSign(appHostDestinationFilePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Delete the destination file so we don't leave an unmodified apphost
|
||||
try
|
||||
{
|
||||
File.Delete(appHostDestinationFilePath);
|
||||
}
|
||||
catch (Exception failedToDeleteEx)
|
||||
{
|
||||
throw new AggregateException(ex, failedToDeleteEx);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the current AppHost as a single-file bundle.
|
||||
/// </summary>
|
||||
/// <param name="appHostPath">The path of Apphost template, which has the place holder</param>
|
||||
/// <param name="bundleHeaderOffset">The offset to the location of bundle header</param>
|
||||
public static void SetAsBundle(
|
||||
string appHostPath,
|
||||
long bundleHeaderOffset)
|
||||
{
|
||||
byte[] bundleHeaderPlaceholder = {
|
||||
// 8 bytes represent the bundle header-offset
|
||||
// Zero for non-bundle apphosts (default).
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// 32 bytes represent the bundle signature: SHA-256 for ".net core bundle"
|
||||
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
|
||||
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
|
||||
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
|
||||
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
|
||||
};
|
||||
|
||||
// Re-write the destination apphost with the proper contents.
|
||||
RetryUtil.RetryOnIOError(() =>
|
||||
BinaryUtils.SearchAndReplace(appHostPath,
|
||||
bundleHeaderPlaceholder,
|
||||
BitConverter.GetBytes(bundleHeaderOffset),
|
||||
pad0s: false));
|
||||
|
||||
RetryUtil.RetryOnIOError(() =>
|
||||
MachOUtils.AdjustHeadersForBundle(appHostPath));
|
||||
|
||||
// Memory-mapped write does not updating last write time
|
||||
RetryUtil.RetryOnIOError(() =>
|
||||
File.SetLastWriteTimeUtc(appHostPath, DateTime.UtcNow));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the an AppHost is a single-file bundle
|
||||
/// </summary>
|
||||
/// <param name="appHostFilePath">The path of Apphost to check</param>
|
||||
/// <param name="bundleHeaderOffset">An out parameter containing the offset of the bundle header (if any)</param>
|
||||
/// <returns>True if the AppHost is a single-file bundle, false otherwise</returns>
|
||||
public static bool IsBundle(string appHostFilePath, out long bundleHeaderOffset)
|
||||
{
|
||||
byte[] bundleSignature = {
|
||||
// 32 bytes represent the bundle signature: SHA-256 for ".net core bundle"
|
||||
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
|
||||
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
|
||||
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
|
||||
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
|
||||
};
|
||||
|
||||
long headerOffset = 0;
|
||||
void FindBundleHeader()
|
||||
{
|
||||
using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostFilePath))
|
||||
{
|
||||
using (MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor())
|
||||
{
|
||||
int position = BinaryUtils.SearchInFile(accessor, bundleSignature);
|
||||
if (position == -1)
|
||||
{
|
||||
throw new PlaceHolderNotFoundInAppHostException(bundleSignature);
|
||||
}
|
||||
|
||||
headerOffset = accessor.ReadInt64(position - sizeof(long));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RetryUtil.RetryOnIOError(FindBundleHeader);
|
||||
bundleHeaderOffset = headerOffset;
|
||||
|
||||
return headerOffset != 0;
|
||||
}
|
||||
|
||||
private static void CodeSign(string appHostPath)
|
||||
{
|
||||
Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.OSX));
|
||||
const string codesign = @"/usr/bin/codesign";
|
||||
if (!File.Exists(codesign))
|
||||
return;
|
||||
|
||||
var psi = new ProcessStartInfo()
|
||||
{
|
||||
Arguments = $"-s - \"{appHostPath}\"",
|
||||
FileName = codesign,
|
||||
RedirectStandardError = true,
|
||||
};
|
||||
|
||||
using (var p = Process.Start(psi))
|
||||
{
|
||||
p.WaitForExit();
|
||||
if (p.ExitCode != 0)
|
||||
throw new AppHostSigningException(p.ExitCode, p.StandardError.ReadToEnd());
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
private static extern int chmod(string pathname, int mode);
|
||||
}
|
||||
}
|
||||
26
src/Microsoft.NET.HostModel/AppHost/MachOFormatError.cs
Normal file
26
src/Microsoft.NET.HostModel/AppHost/MachOFormatError.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.NET.HostModel.AppHost
|
||||
{
|
||||
/// <summary>
|
||||
/// Additional details about the failure with caused an AppHostMachOFormatException
|
||||
/// </summary>
|
||||
public enum MachOFormatError
|
||||
{
|
||||
Not64BitExe, // Apphost is expected to be a 64-bit MachO executable
|
||||
DuplicateLinkEdit, // Only one __LINKEDIT segment is expected in the apphost
|
||||
DuplicateSymtab, // Only one SYMTAB is expected in the apphost
|
||||
MissingLinkEdit, // CODE_SIGNATURE command must follow a Segment64 command named __LINKEDIT
|
||||
MissingSymtab, // CODE_SIGNATURE command must follow the SYMTAB command
|
||||
LinkEditNotLast, // __LINKEDIT must be the last segment in the binary layout
|
||||
SymtabNotInLinkEdit, // SYMTAB must within the __LINKEDIT segment!
|
||||
SignNotInLinkEdit, // Signature blob must be within the __LINKEDIT segment!
|
||||
SignCommandNotLast, // CODE_SIGNATURE command must be the last command
|
||||
SignBlobNotLast, // Signature blob must be at the very end of the file
|
||||
SignDoesntFollowSymtab, // Signature blob must immediately follow the Symtab
|
||||
MemoryMapAccessFault, // Error reading the memory-mapped apphost
|
||||
InvalidUTF8, // UTF8 decoding failed
|
||||
SignNotRemoved, // Signature not removed from the host (while processing a single-file bundle)
|
||||
}
|
||||
}
|
||||
446
src/Microsoft.NET.HostModel/AppHost/MachOUtils.cs
Normal file
446
src/Microsoft.NET.HostModel/AppHost/MachOUtils.cs
Normal file
@@ -0,0 +1,446 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.NET.HostModel.AppHost
|
||||
{
|
||||
internal static class MachOUtils
|
||||
{
|
||||
// The MachO Headers are copied from
|
||||
// https://opensource.apple.com/source/cctools/cctools-870/include/mach-o/loader.h
|
||||
//
|
||||
// The data fields and enumerations match the structure definitions in the above file,
|
||||
// and hence do not conform to C# CoreFx naming style.
|
||||
|
||||
private enum Magic : uint
|
||||
{
|
||||
MH_MAGIC = 0xfeedface,
|
||||
MH_CIGAM = 0xcefaedfe,
|
||||
MH_MAGIC_64 = 0xfeedfacf,
|
||||
MH_CIGAM_64 = 0xcffaedfe
|
||||
}
|
||||
|
||||
private enum FileType : uint
|
||||
{
|
||||
MH_EXECUTE = 0x2
|
||||
}
|
||||
|
||||
#pragma warning disable 0649
|
||||
private struct MachHeader
|
||||
{
|
||||
public Magic magic;
|
||||
public int cputype;
|
||||
public int cpusubtype;
|
||||
public FileType filetype;
|
||||
public uint ncmds;
|
||||
public uint sizeofcmds;
|
||||
public uint flags;
|
||||
public uint reserved;
|
||||
|
||||
public bool Is64BitExecutable()
|
||||
{
|
||||
return magic == Magic.MH_MAGIC_64 && filetype == FileType.MH_EXECUTE;
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
switch (magic)
|
||||
{
|
||||
case Magic.MH_CIGAM:
|
||||
case Magic.MH_CIGAM_64:
|
||||
case Magic.MH_MAGIC:
|
||||
case Magic.MH_MAGIC_64:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum Command : uint
|
||||
{
|
||||
LC_SYMTAB = 0x2,
|
||||
LC_SEGMENT_64 = 0x19,
|
||||
LC_CODE_SIGNATURE = 0x1d,
|
||||
}
|
||||
|
||||
private struct LoadCommand
|
||||
{
|
||||
public Command cmd;
|
||||
public uint cmdsize;
|
||||
}
|
||||
|
||||
// The linkedit_data_command contains the offsets and sizes of a blob
|
||||
// of data in the __LINKEDIT segment (including LC_CODE_SIGNATURE).
|
||||
private struct LinkEditDataCommand
|
||||
{
|
||||
public Command cmd;
|
||||
public uint cmdsize;
|
||||
public uint dataoff;
|
||||
public uint datasize;
|
||||
}
|
||||
|
||||
private struct SymtabCommand
|
||||
{
|
||||
public uint cmd;
|
||||
public uint cmdsize;
|
||||
public uint symoff;
|
||||
public uint nsyms;
|
||||
public uint stroff;
|
||||
public uint strsize;
|
||||
};
|
||||
|
||||
private unsafe struct SegmentCommand64
|
||||
{
|
||||
public Command cmd;
|
||||
public uint cmdsize;
|
||||
public fixed byte segname[16];
|
||||
public ulong vmaddr;
|
||||
public ulong vmsize;
|
||||
public ulong fileoff;
|
||||
public ulong filesize;
|
||||
public int maxprot;
|
||||
public int initprot;
|
||||
public uint nsects;
|
||||
public uint flags;
|
||||
|
||||
public string SegName
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (byte* p = segname)
|
||||
{
|
||||
int len = 0;
|
||||
while (*(p + len) != 0 && len++ < 16) ;
|
||||
|
||||
try
|
||||
{
|
||||
return Encoding.UTF8.GetString(p, len);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
throw new AppHostMachOFormatException(MachOFormatError.InvalidUTF8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning restore 0649
|
||||
|
||||
private static void Verify(bool condition, MachOFormatError error)
|
||||
{
|
||||
if (!condition)
|
||||
{
|
||||
throw new AppHostMachOFormatException(error);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsMachOImage(string filePath)
|
||||
{
|
||||
using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
|
||||
{
|
||||
if (reader.BaseStream.Length < 256) // Header size
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint magic = reader.ReadUInt32();
|
||||
return Enum.IsDefined(typeof(Magic), magic);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This Method is a utility to remove the code-signature (if any)
|
||||
/// from a MachO AppHost binary.
|
||||
///
|
||||
/// The tool assumes the following layout of the executable:
|
||||
///
|
||||
/// * MachoHeader (64-bit, executable, not swapped integers)
|
||||
/// * LoadCommands
|
||||
/// LC_SEGMENT_64 (__PAGEZERO)
|
||||
/// LC_SEGMENT_64 (__TEXT)
|
||||
/// LC_SEGMENT_64 (__DATA)
|
||||
/// LC_SEGMENT_64 (__LINKEDIT)
|
||||
/// ...
|
||||
/// LC_SYMTAB
|
||||
/// ...
|
||||
/// LC_CODE_SIGNATURE (last)
|
||||
///
|
||||
/// * ... Different Segments ...
|
||||
///
|
||||
/// * The __LINKEDIT Segment (last)
|
||||
/// * ... Different sections ...
|
||||
/// * SYMTAB
|
||||
/// * (Some alignment bytes)
|
||||
/// * The Code-signature
|
||||
///
|
||||
/// In order to remove the signature, the method:
|
||||
/// - Removes (zeros out) the LC_CODE_SIGNATURE command
|
||||
/// - Adjusts the size and count of the load commands in the header
|
||||
/// - Truncates the size of the __LINKEDIT segment to the end of SYMTAB
|
||||
/// - Truncates the apphost file to the end of the __LINKEDIT segment
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="stream">Stream containing the AppHost</param>
|
||||
/// <returns>
|
||||
/// True if
|
||||
/// - The input is a MachO binary, and
|
||||
/// - It is a signed binary, and
|
||||
/// - The signature was successfully removed
|
||||
/// False otherwise
|
||||
/// </returns>
|
||||
/// <exception cref="AppHostMachOFormatException">
|
||||
/// The input is a MachO file, but doesn't match the expect format of the AppHost.
|
||||
/// </exception>
|
||||
public static unsafe bool RemoveSignature(FileStream stream)
|
||||
{
|
||||
uint signatureSize = 0;
|
||||
using (var mappedFile = MemoryMappedFile.CreateFromFile(stream,
|
||||
mapName: null,
|
||||
capacity: 0,
|
||||
MemoryMappedFileAccess.ReadWrite,
|
||||
HandleInheritability.None,
|
||||
leaveOpen: true))
|
||||
{
|
||||
using (var accessor = mappedFile.CreateViewAccessor())
|
||||
{
|
||||
byte* file = null;
|
||||
try
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file);
|
||||
Verify(file != null, MachOFormatError.MemoryMapAccessFault);
|
||||
|
||||
MachHeader* header = (MachHeader*)file;
|
||||
|
||||
if (!header->IsValid())
|
||||
{
|
||||
// Not a MachO file.
|
||||
return false;
|
||||
}
|
||||
|
||||
Verify(header->Is64BitExecutable(), MachOFormatError.Not64BitExe);
|
||||
|
||||
file += sizeof(MachHeader);
|
||||
SegmentCommand64* linkEdit = null;
|
||||
SymtabCommand* symtab = null;
|
||||
LinkEditDataCommand* signature = null;
|
||||
|
||||
for (uint i = 0; i < header->ncmds; i++)
|
||||
{
|
||||
LoadCommand* command = (LoadCommand*)file;
|
||||
if (command->cmd == Command.LC_SEGMENT_64)
|
||||
{
|
||||
SegmentCommand64* segment = (SegmentCommand64*)file;
|
||||
if (segment->SegName.Equals("__LINKEDIT"))
|
||||
{
|
||||
Verify(linkEdit == null, MachOFormatError.DuplicateLinkEdit);
|
||||
linkEdit = segment;
|
||||
}
|
||||
}
|
||||
else if (command->cmd == Command.LC_SYMTAB)
|
||||
{
|
||||
Verify(symtab == null, MachOFormatError.DuplicateSymtab);
|
||||
symtab = (SymtabCommand*)command;
|
||||
}
|
||||
else if (command->cmd == Command.LC_CODE_SIGNATURE)
|
||||
{
|
||||
Verify(i == header->ncmds - 1, MachOFormatError.SignCommandNotLast);
|
||||
signature = (LinkEditDataCommand*)command;
|
||||
break;
|
||||
}
|
||||
|
||||
file += command->cmdsize;
|
||||
}
|
||||
|
||||
if (signature != null)
|
||||
{
|
||||
Verify(linkEdit != null, MachOFormatError.MissingLinkEdit);
|
||||
Verify(symtab != null, MachOFormatError.MissingSymtab);
|
||||
|
||||
var symtabEnd = symtab->stroff + symtab->strsize;
|
||||
var linkEditEnd = linkEdit->fileoff + linkEdit->filesize;
|
||||
var signatureEnd = signature->dataoff + signature->datasize;
|
||||
var fileEnd = (ulong)stream.Length;
|
||||
|
||||
Verify(linkEditEnd == fileEnd, MachOFormatError.LinkEditNotLast);
|
||||
Verify(signatureEnd == fileEnd, MachOFormatError.SignBlobNotLast);
|
||||
|
||||
Verify(symtab->symoff > linkEdit->fileoff, MachOFormatError.SymtabNotInLinkEdit);
|
||||
Verify(signature->dataoff > linkEdit->fileoff, MachOFormatError.SignNotInLinkEdit);
|
||||
|
||||
// The signature blob immediately follows the symtab blob,
|
||||
// except for a few bytes of padding.
|
||||
Verify(signature->dataoff >= symtabEnd && signature->dataoff - symtabEnd < 32, MachOFormatError.SignBlobNotLast);
|
||||
|
||||
// Remove the signature command
|
||||
header->ncmds--;
|
||||
header->sizeofcmds -= signature->cmdsize;
|
||||
Unsafe.InitBlock(signature, 0, signature->cmdsize);
|
||||
|
||||
// Remove the signature blob (note for truncation)
|
||||
signatureSize = (uint)(fileEnd - symtabEnd);
|
||||
|
||||
// Adjust the __LINKEDIT segment load command
|
||||
linkEdit->filesize -= signatureSize;
|
||||
|
||||
// codesign --remove-signature doesn't reset the vmsize.
|
||||
// Setting the vmsize here makes the output bin-equal with the original
|
||||
// unsigned apphost (and not bin-equal with a signed-unsigned-apphost).
|
||||
linkEdit->vmsize = linkEdit->filesize;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (file != null)
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (signatureSize != 0)
|
||||
{
|
||||
// The signature was removed, update the file length
|
||||
stream.SetLength(stream.Length - signatureSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This Method is a utility to adjust the apphost MachO-header
|
||||
/// to include the bytes added by the single-file bundler at the end of the file.
|
||||
///
|
||||
/// The tool assumes the following layout of the executable
|
||||
///
|
||||
/// * MachoHeader (64-bit, executable, not swapped integers)
|
||||
/// * LoadCommands
|
||||
/// LC_SEGMENT_64 (__PAGEZERO)
|
||||
/// LC_SEGMENT_64 (__TEXT)
|
||||
/// LC_SEGMENT_64 (__DATA)
|
||||
/// LC_SEGMENT_64 (__LINKEDIT)
|
||||
/// ...
|
||||
/// LC_SYMTAB
|
||||
///
|
||||
/// * ... Different Segments
|
||||
///
|
||||
/// * The __LINKEDIT Segment (last)
|
||||
/// * ... Different sections ...
|
||||
/// * SYMTAB (last)
|
||||
///
|
||||
/// The MAC codesign tool places several restrictions on the layout
|
||||
/// * The __LINKEDIT segment must be the last one
|
||||
/// * The __LINKEDIT segment must cover the end of the file
|
||||
/// * All bytes in the __LINKEDIT segment are used by other linkage commands
|
||||
/// (ex: symbol/string table, dynamic load information etc)
|
||||
///
|
||||
/// In order to circumvent these restrictions, we:
|
||||
/// * Extend the __LINKEDIT segment to include the bundle-data
|
||||
/// * Extend the string table to include all the bundle-data
|
||||
/// (that is, the bundle-data appear as strings to the loader/codesign tool).
|
||||
///
|
||||
/// This method has certain limitations:
|
||||
/// * The bytes for the bundler may be unnecessarily loaded at startup
|
||||
/// * Tools that process the string table may be confused (?)
|
||||
/// * The string table size is limited to 4GB. Bundles larger than that size
|
||||
/// cannot be accomodated by this utility.
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the AppHost</param>
|
||||
/// <returns>
|
||||
/// True if
|
||||
/// - The input is a MachO binary, and
|
||||
/// - The additional bytes were successfully accomodated within the MachO segments.
|
||||
/// False otherwise
|
||||
/// </returns>
|
||||
/// <exception cref="AppHostMachOFormatException">
|
||||
/// The input is a MachO file, but doesn't match the expect format of the AppHost.
|
||||
/// </exception>
|
||||
public static unsafe bool AdjustHeadersForBundle(string filePath)
|
||||
{
|
||||
ulong fileLength = (ulong)new FileInfo(filePath).Length;
|
||||
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
|
||||
{
|
||||
using (var accessor = mappedFile.CreateViewAccessor())
|
||||
{
|
||||
byte* file = null;
|
||||
try
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file);
|
||||
Verify(file != null, MachOFormatError.MemoryMapAccessFault);
|
||||
|
||||
MachHeader* header = (MachHeader*)file;
|
||||
|
||||
if (!header->IsValid())
|
||||
{
|
||||
// Not a MachO file.
|
||||
return false;
|
||||
}
|
||||
|
||||
Verify(header->Is64BitExecutable(), MachOFormatError.Not64BitExe);
|
||||
|
||||
file += sizeof(MachHeader);
|
||||
SegmentCommand64* linkEdit = null;
|
||||
SymtabCommand* symtab = null;
|
||||
LinkEditDataCommand* signature = null;
|
||||
|
||||
for (uint i = 0; i < header->ncmds; i++)
|
||||
{
|
||||
LoadCommand* command = (LoadCommand*)file;
|
||||
if (command->cmd == Command.LC_SEGMENT_64)
|
||||
{
|
||||
SegmentCommand64* segment = (SegmentCommand64*)file;
|
||||
if (segment->SegName.Equals("__LINKEDIT"))
|
||||
{
|
||||
Verify(linkEdit == null, MachOFormatError.DuplicateLinkEdit);
|
||||
linkEdit = segment;
|
||||
}
|
||||
}
|
||||
else if (command->cmd == Command.LC_SYMTAB)
|
||||
{
|
||||
Verify(symtab == null, MachOFormatError.DuplicateSymtab);
|
||||
symtab = (SymtabCommand*)command;
|
||||
}
|
||||
|
||||
file += command->cmdsize;
|
||||
}
|
||||
|
||||
Verify(linkEdit != null, MachOFormatError.MissingLinkEdit);
|
||||
Verify(symtab != null, MachOFormatError.MissingSymtab);
|
||||
|
||||
// Update the string table to include bundle-data
|
||||
ulong newStringTableSize = fileLength - symtab->stroff;
|
||||
if (newStringTableSize > uint.MaxValue)
|
||||
{
|
||||
// Too big, too bad;
|
||||
return false;
|
||||
}
|
||||
symtab->strsize = (uint)newStringTableSize;
|
||||
|
||||
// Update the __LINKEDIT segment to include bundle-data
|
||||
linkEdit->filesize = fileLength - linkEdit->fileoff;
|
||||
linkEdit->vmsize = linkEdit->filesize;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (file != null)
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
180
src/Microsoft.NET.HostModel/AppHost/PEUtils.cs
Normal file
180
src/Microsoft.NET.HostModel/AppHost/PEUtils.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
|
||||
namespace Microsoft.NET.HostModel.AppHost
|
||||
{
|
||||
public static class PEUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// The first two bytes of a PE file are a constant signature.
|
||||
/// </summary>
|
||||
private const ushort PEFileSignature = 0x5A4D;
|
||||
|
||||
/// <summary>
|
||||
/// The offset of the PE header pointer in the DOS header.
|
||||
/// </summary>
|
||||
private const int PEHeaderPointerOffset = 0x3C;
|
||||
|
||||
/// <summary>
|
||||
/// The offset of the Subsystem field in the PE header.
|
||||
/// </summary>
|
||||
private const int SubsystemOffset = 0x5C;
|
||||
|
||||
/// <summary>
|
||||
/// The value of the sybsystem field which indicates Windows GUI (Graphical UI)
|
||||
/// </summary>
|
||||
private const ushort WindowsGUISubsystem = 0x2;
|
||||
|
||||
/// <summary>
|
||||
/// The value of the subsystem field which indicates Windows CUI (Console)
|
||||
/// </summary>
|
||||
private const ushort WindowsCUISubsystem = 0x3;
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the apphost file is a windows PE image by looking at the first few bytes.
|
||||
/// </summary>
|
||||
/// <param name="accessor">The memory accessor which has the apphost file opened.</param>
|
||||
/// <returns>true if the accessor represents a PE image, false otherwise.</returns>
|
||||
internal static unsafe bool IsPEImage(MemoryMappedViewAccessor accessor)
|
||||
{
|
||||
byte* pointer = null;
|
||||
|
||||
try
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
|
||||
byte* bytes = pointer + accessor.PointerOffset;
|
||||
|
||||
// https://en.wikipedia.org/wiki/Portable_Executable
|
||||
// Validate that we're looking at Windows PE file
|
||||
if (((ushort*)bytes)[0] != PEFileSignature || accessor.Capacity < PEHeaderPointerOffset + sizeof(uint))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pointer != null)
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsPEImage(string filePath)
|
||||
{
|
||||
using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
|
||||
{
|
||||
if (reader.BaseStream.Length < PEHeaderPointerOffset + sizeof(uint))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ushort signature = reader.ReadUInt16();
|
||||
return signature == PEFileSignature;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method will attempt to set the subsystem to GUI. The apphost file should be a windows PE file.
|
||||
/// </summary>
|
||||
/// <param name="accessor">The memory accessor which has the apphost file opened.</param>
|
||||
internal static unsafe void SetWindowsGraphicalUserInterfaceBit(MemoryMappedViewAccessor accessor)
|
||||
{
|
||||
byte* pointer = null;
|
||||
|
||||
try
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
|
||||
byte* bytes = pointer + accessor.PointerOffset;
|
||||
|
||||
// https://en.wikipedia.org/wiki/Portable_Executable
|
||||
uint peHeaderOffset = ((uint*)(bytes + PEHeaderPointerOffset))[0];
|
||||
|
||||
if (accessor.Capacity < peHeaderOffset + SubsystemOffset + sizeof(ushort))
|
||||
{
|
||||
throw new AppHostNotPEFileException();
|
||||
}
|
||||
|
||||
ushort* subsystem = ((ushort*)(bytes + peHeaderOffset + SubsystemOffset));
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/Debug/pe-format#windows-subsystem
|
||||
// The subsystem of the prebuilt apphost should be set to CUI
|
||||
if (subsystem[0] != WindowsCUISubsystem)
|
||||
{
|
||||
throw new AppHostNotCUIException();
|
||||
}
|
||||
|
||||
// Set the subsystem to GUI
|
||||
subsystem[0] = WindowsGUISubsystem;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pointer != null)
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe void SetWindowsGraphicalUserInterfaceBit(string filePath)
|
||||
{
|
||||
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
|
||||
{
|
||||
using (var accessor = mappedFile.CreateViewAccessor())
|
||||
{
|
||||
SetWindowsGraphicalUserInterfaceBit(accessor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method will return the subsystem CUI/GUI value. The apphost file should be a windows PE file.
|
||||
/// </summary>
|
||||
/// <param name="accessor">The memory accessor which has the apphost file opened.</param>
|
||||
internal static unsafe ushort GetWindowsGraphicalUserInterfaceBit(MemoryMappedViewAccessor accessor)
|
||||
{
|
||||
byte* pointer = null;
|
||||
|
||||
try
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
|
||||
byte* bytes = pointer + accessor.PointerOffset;
|
||||
|
||||
// https://en.wikipedia.org/wiki/Portable_Executable
|
||||
uint peHeaderOffset = ((uint*)(bytes + PEHeaderPointerOffset))[0];
|
||||
|
||||
if (accessor.Capacity < peHeaderOffset + SubsystemOffset + sizeof(ushort))
|
||||
{
|
||||
throw new AppHostNotPEFileException();
|
||||
}
|
||||
|
||||
ushort* subsystem = ((ushort*)(bytes + peHeaderOffset + SubsystemOffset));
|
||||
|
||||
return subsystem[0];
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pointer != null)
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe ushort GetWindowsGraphicalUserInterfaceBit(string filePath)
|
||||
{
|
||||
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
|
||||
{
|
||||
using (var accessor = mappedFile.CreateViewAccessor())
|
||||
{
|
||||
return GetWindowsGraphicalUserInterfaceBit(accessor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.NET.HostModel.AppHost
|
||||
{
|
||||
/// <summary>
|
||||
/// Unable to use input file as a valid application host executable, as it does not contain
|
||||
/// the expected placeholder byte sequence.
|
||||
/// </summary>
|
||||
public class PlaceHolderNotFoundInAppHostException : AppHostUpdateException
|
||||
{
|
||||
public byte[] MissingPattern { get; }
|
||||
public PlaceHolderNotFoundInAppHostException(byte[] pattern)
|
||||
{
|
||||
MissingPattern = pattern;
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/Microsoft.NET.HostModel/AppHost/RetryUtil.cs
Normal file
98
src/Microsoft.NET.HostModel/AppHost/RetryUtil.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.NET.HostModel
|
||||
{
|
||||
/// <summary>
|
||||
/// HostModel library implements several services for updating the AppHost DLL.
|
||||
/// These updates involve multiple file open/close operations.
|
||||
/// An Antivirus scanner may intercept in-between and lock the file,
|
||||
/// causing the operations to fail with IO-Error.
|
||||
/// So, the operations are retried a few times on failures such as
|
||||
/// - IOException
|
||||
/// - Failure with Win32 errors indicating file-lock
|
||||
/// </summary>
|
||||
public static class RetryUtil
|
||||
{
|
||||
public const int NumberOfRetries = 500;
|
||||
public const int NumMilliSecondsToWait = 100;
|
||||
|
||||
public static void RetryOnIOError(Action func)
|
||||
{
|
||||
for (int i = 1; i <= NumberOfRetries; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
func();
|
||||
break;
|
||||
}
|
||||
catch (IOException) when (i < NumberOfRetries)
|
||||
{
|
||||
Thread.Sleep(NumMilliSecondsToWait);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RetryOnWin32Error(Action func)
|
||||
{
|
||||
static bool IsKnownIrrecoverableError(int hresult)
|
||||
{
|
||||
// Error codes are defined in winerror.h
|
||||
// The error code is stored in the lowest 16 bits of the HResult
|
||||
|
||||
switch (hresult & 0xffff)
|
||||
{
|
||||
case 0x00000001: // ERROR_INVALID_FUNCTION
|
||||
case 0x00000002: // ERROR_FILE_NOT_FOUND
|
||||
case 0x00000003: // ERROR_PATH_NOT_FOUND
|
||||
case 0x00000006: // ERROR_INVALID_HANDLE
|
||||
case 0x00000008: // ERROR_NOT_ENOUGH_MEMORY
|
||||
case 0x0000000B: // ERROR_BAD_FORMAT
|
||||
case 0x0000000E: // ERROR_OUTOFMEMORY
|
||||
case 0x0000000F: // ERROR_INVALID_DRIVE
|
||||
case 0x00000012: // ERROR_NO_MORE_FILES
|
||||
case 0x00000035: // ERROR_BAD_NETPATH
|
||||
case 0x00000057: // ERROR_INVALID_PARAMETER
|
||||
case 0x00000071: // ERROR_NO_MORE_SEARCH_HANDLES
|
||||
case 0x00000072: // ERROR_INVALID_TARGET_HANDLE
|
||||
case 0x00000078: // ERROR_CALL_NOT_IMPLEMENTED
|
||||
case 0x0000007B: // ERROR_INVALID_NAME
|
||||
case 0x0000007C: // ERROR_INVALID_LEVEL
|
||||
case 0x0000007D: // ERROR_NO_VOLUME_LABEL
|
||||
case 0x0000009A: // ERROR_LABEL_TOO_LONG
|
||||
case 0x000000A0: // ERROR_BAD_ARGUMENTS
|
||||
case 0x000000A1: // ERROR_BAD_PATHNAME
|
||||
case 0x000000CE: // ERROR_FILENAME_EXCED_RANGE
|
||||
case 0x000000DF: // ERROR_FILE_TOO_LARGE
|
||||
case 0x000003ED: // ERROR_UNRECOGNIZED_VOLUME
|
||||
case 0x000003EE: // ERROR_FILE_INVALID
|
||||
case 0x00000651: // ERROR_DEVICE_REMOVED
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 1; i <= NumberOfRetries; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
func();
|
||||
break;
|
||||
}
|
||||
catch (HResultException hrex)
|
||||
when (i < NumberOfRetries && !IsKnownIrrecoverableError(hrex.Win32HResult))
|
||||
{
|
||||
Thread.Sleep(NumMilliSecondsToWait);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/Microsoft.NET.HostModel/Bundle/BundleOptions.cs
Normal file
22
src/Microsoft.NET.HostModel/Bundle/BundleOptions.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.NET.HostModel.Bundle
|
||||
{
|
||||
/// <summary>
|
||||
/// BundleOptions: Optional settings for configuring the type of files
|
||||
/// included in the single file bundle.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum BundleOptions
|
||||
{
|
||||
None = 0,
|
||||
BundleNativeBinaries = 1,
|
||||
BundleOtherFiles = 2,
|
||||
BundleSymbolFiles = 4,
|
||||
BundleAllContent = BundleNativeBinaries | BundleOtherFiles,
|
||||
EnableCompression = 8,
|
||||
};
|
||||
}
|
||||
342
src/Microsoft.NET.HostModel/Bundle/Bundler.cs
Normal file
342
src/Microsoft.NET.HostModel/Bundle/Bundler.cs
Normal file
@@ -0,0 +1,342 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Reflection.PortableExecutable;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.NET.HostModel.AppHost;
|
||||
|
||||
namespace Microsoft.NET.HostModel.Bundle
|
||||
{
|
||||
/// <summary>
|
||||
/// Bundler: Functionality to embed the managed app and its dependencies
|
||||
/// into the host native binary.
|
||||
/// </summary>
|
||||
public class Bundler
|
||||
{
|
||||
public const uint BundlerMajorVersion = 6;
|
||||
public const uint BundlerMinorVersion = 0;
|
||||
|
||||
private readonly string HostName;
|
||||
private readonly string OutputDir;
|
||||
private readonly string DepsJson;
|
||||
private readonly string RuntimeConfigJson;
|
||||
private readonly string RuntimeConfigDevJson;
|
||||
|
||||
private readonly Trace Tracer;
|
||||
public readonly Manifest BundleManifest;
|
||||
private readonly TargetInfo Target;
|
||||
private readonly BundleOptions Options;
|
||||
|
||||
public Bundler(string hostName,
|
||||
string outputDir,
|
||||
BundleOptions options = BundleOptions.None,
|
||||
OSPlatform? targetOS = null,
|
||||
Architecture? targetArch = null,
|
||||
Version targetFrameworkVersion = null,
|
||||
bool diagnosticOutput = false,
|
||||
string appAssemblyName = null)
|
||||
{
|
||||
Tracer = new Trace(diagnosticOutput);
|
||||
|
||||
HostName = hostName;
|
||||
OutputDir = Path.GetFullPath(string.IsNullOrEmpty(outputDir) ? Environment.CurrentDirectory : outputDir);
|
||||
Target = new TargetInfo(targetOS, targetArch, targetFrameworkVersion);
|
||||
|
||||
if (Target.BundleMajorVersion < 6 &&
|
||||
(options & BundleOptions.EnableCompression) != 0)
|
||||
{
|
||||
throw new ArgumentException("Compression requires framework version 6.0 or above", nameof(options));
|
||||
}
|
||||
|
||||
appAssemblyName ??= Target.GetAssemblyName(hostName);
|
||||
DepsJson = appAssemblyName + ".deps.json";
|
||||
RuntimeConfigJson = appAssemblyName + ".runtimeconfig.json";
|
||||
RuntimeConfigDevJson = appAssemblyName + ".runtimeconfig.dev.json";
|
||||
|
||||
BundleManifest = new Manifest(Target.BundleMajorVersion, netcoreapp3CompatMode: options.HasFlag(BundleOptions.BundleAllContent));
|
||||
Options = Target.DefaultOptions | options;
|
||||
}
|
||||
|
||||
private bool ShouldCompress(FileType type)
|
||||
{
|
||||
if (!Options.HasFlag(BundleOptions.EnableCompression))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case FileType.DepsJson:
|
||||
case FileType.RuntimeConfigJson:
|
||||
return false;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Embed 'file' into 'bundle'
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// startOffset: offset of the start 'file' within 'bundle'
|
||||
/// compressedSize: size of the compressed data, if entry was compressed, otherwise 0
|
||||
/// </returns>
|
||||
private (long startOffset, long compressedSize) AddToBundle(Stream bundle, Stream file, FileType type)
|
||||
{
|
||||
long startOffset = bundle.Position;
|
||||
if (ShouldCompress(type))
|
||||
{
|
||||
long fileLength = file.Length;
|
||||
file.Position = 0;
|
||||
|
||||
// We use DeflateStream here.
|
||||
// It uses GZip algorithm, but with a trivial header that does not contain file info.
|
||||
using (DeflateStream compressionStream = new DeflateStream(bundle, CompressionLevel.Optimal, leaveOpen: true))
|
||||
{
|
||||
file.CopyTo(compressionStream);
|
||||
}
|
||||
|
||||
long compressedSize = bundle.Position - startOffset;
|
||||
if (compressedSize < fileLength * 0.75)
|
||||
{
|
||||
return (startOffset, compressedSize);
|
||||
}
|
||||
|
||||
// compression rate was not good enough
|
||||
// roll back the bundle offset and let the uncompressed code path take care of the entry.
|
||||
bundle.Seek(startOffset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
if (type == FileType.Assembly)
|
||||
{
|
||||
long misalignment = (bundle.Position % Target.AssemblyAlignment);
|
||||
|
||||
if (misalignment != 0)
|
||||
{
|
||||
long padding = Target.AssemblyAlignment - misalignment;
|
||||
bundle.Position += padding;
|
||||
}
|
||||
}
|
||||
|
||||
file.Position = 0;
|
||||
startOffset = bundle.Position;
|
||||
file.CopyTo(bundle);
|
||||
|
||||
return (startOffset, 0);
|
||||
}
|
||||
|
||||
private bool IsHost(string fileRelativePath)
|
||||
{
|
||||
return fileRelativePath.Equals(HostName);
|
||||
}
|
||||
|
||||
private bool ShouldIgnore(string fileRelativePath)
|
||||
{
|
||||
return fileRelativePath.Equals(RuntimeConfigDevJson);
|
||||
}
|
||||
|
||||
private bool ShouldExclude(FileType type, string relativePath)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case FileType.Assembly:
|
||||
case FileType.DepsJson:
|
||||
case FileType.RuntimeConfigJson:
|
||||
return false;
|
||||
|
||||
case FileType.NativeBinary:
|
||||
return !Options.HasFlag(BundleOptions.BundleNativeBinaries) || Target.ShouldExclude(relativePath);
|
||||
|
||||
case FileType.Symbols:
|
||||
return !Options.HasFlag(BundleOptions.BundleSymbolFiles);
|
||||
|
||||
case FileType.Unknown:
|
||||
return !Options.HasFlag(BundleOptions.BundleOtherFiles);
|
||||
|
||||
default:
|
||||
Debug.Assert(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsAssembly(string path, out bool isPE)
|
||||
{
|
||||
isPE = false;
|
||||
|
||||
using (FileStream file = File.OpenRead(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
PEReader peReader = new PEReader(file);
|
||||
CorHeader corHeader = peReader.PEHeaders.CorHeader;
|
||||
|
||||
isPE = true; // If peReader.PEHeaders doesn't throw, it is a valid PEImage
|
||||
return corHeader != null;
|
||||
}
|
||||
catch (BadImageFormatException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private FileType InferType(FileSpec fileSpec)
|
||||
{
|
||||
if (fileSpec.BundleRelativePath.Equals(DepsJson))
|
||||
{
|
||||
return FileType.DepsJson;
|
||||
}
|
||||
|
||||
if (fileSpec.BundleRelativePath.Equals(RuntimeConfigJson))
|
||||
{
|
||||
return FileType.RuntimeConfigJson;
|
||||
}
|
||||
|
||||
if (Path.GetExtension(fileSpec.BundleRelativePath).ToLowerInvariant().Equals(".pdb"))
|
||||
{
|
||||
return FileType.Symbols;
|
||||
}
|
||||
|
||||
bool isPE;
|
||||
if (IsAssembly(fileSpec.SourcePath, out isPE))
|
||||
{
|
||||
return FileType.Assembly;
|
||||
}
|
||||
|
||||
bool isNativeBinary = Target.IsWindows ? isPE : Target.IsNativeBinary(fileSpec.SourcePath);
|
||||
|
||||
if (isNativeBinary)
|
||||
{
|
||||
return FileType.NativeBinary;
|
||||
}
|
||||
|
||||
return FileType.Unknown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a bundle, given the specification of embedded files
|
||||
/// </summary>
|
||||
/// <param name="fileSpecs">
|
||||
/// An enumeration FileSpecs for the files to be embedded.
|
||||
///
|
||||
/// Files in fileSpecs that are not bundled within the single file bundle,
|
||||
/// and should be published as separate files are marked as "IsExcluded" by this method.
|
||||
/// This doesn't include unbundled files that should be dropped, and not publised as output.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The full path the the generated bundle file
|
||||
/// </returns>
|
||||
/// <exceptions>
|
||||
/// ArgumentException if input is invalid
|
||||
/// IOExceptions and ArgumentExceptions from callees flow to the caller.
|
||||
/// </exceptions>
|
||||
public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
|
||||
{
|
||||
Tracer.Log($"Bundler Version: {BundlerMajorVersion}.{BundlerMinorVersion}");
|
||||
Tracer.Log($"Bundle Version: {BundleManifest.BundleVersion}");
|
||||
Tracer.Log($"Target Runtime: {Target}");
|
||||
Tracer.Log($"Bundler Options: {Options}");
|
||||
|
||||
if (fileSpecs.Any(x => !x.IsValid()))
|
||||
{
|
||||
throw new ArgumentException("Invalid input specification: Found entry with empty source-path or bundle-relative-path.");
|
||||
}
|
||||
|
||||
string hostSource;
|
||||
try
|
||||
{
|
||||
hostSource = fileSpecs.Where(x => x.BundleRelativePath.Equals(HostName)).Single().SourcePath;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
throw new ArgumentException("Invalid input specification: Must specify the host binary");
|
||||
}
|
||||
|
||||
string bundlePath = Path.Combine(OutputDir, HostName);
|
||||
if (File.Exists(bundlePath))
|
||||
{
|
||||
Tracer.Log($"Ovewriting existing File {bundlePath}");
|
||||
}
|
||||
|
||||
BinaryUtils.CopyFile(hostSource, bundlePath);
|
||||
|
||||
// Note: We're comparing file paths both on the OS we're running on as well as on the target OS for the app
|
||||
// We can't really make assumptions about the file systems (even on Linux there can be case insensitive file systems
|
||||
// and vice versa for Windows). So it's safer to do case sensitive comparison everywhere.
|
||||
var relativePathToSpec = new Dictionary<string, FileSpec>(StringComparer.Ordinal);
|
||||
|
||||
long headerOffset = 0;
|
||||
using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(bundlePath)))
|
||||
{
|
||||
Stream bundle = writer.BaseStream;
|
||||
bundle.Position = bundle.Length;
|
||||
|
||||
foreach (var fileSpec in fileSpecs)
|
||||
{
|
||||
string relativePath = fileSpec.BundleRelativePath;
|
||||
|
||||
if (IsHost(relativePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ShouldIgnore(relativePath))
|
||||
{
|
||||
Tracer.Log($"Ignore: {relativePath}");
|
||||
continue;
|
||||
}
|
||||
|
||||
FileType type = InferType(fileSpec);
|
||||
|
||||
if (ShouldExclude(type, relativePath))
|
||||
{
|
||||
Tracer.Log($"Exclude [{type}]: {relativePath}");
|
||||
fileSpec.Excluded = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (relativePathToSpec.TryGetValue(fileSpec.BundleRelativePath, out var existingFileSpec))
|
||||
{
|
||||
if (!string.Equals(fileSpec.SourcePath, existingFileSpec.SourcePath, StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException($"Invalid input specification: Found entries '{fileSpec.SourcePath}' and '{existingFileSpec.SourcePath}' with the same BundleRelativePath '{fileSpec.BundleRelativePath}'");
|
||||
}
|
||||
|
||||
// Exact duplicate - intentionally skip and don't include a second copy in the bundle
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
relativePathToSpec.Add(fileSpec.BundleRelativePath, fileSpec);
|
||||
}
|
||||
|
||||
using (FileStream file = File.OpenRead(fileSpec.SourcePath))
|
||||
{
|
||||
FileType targetType = Target.TargetSpecificFileType(type);
|
||||
(long startOffset, long compressedSize) = AddToBundle(bundle, file, targetType);
|
||||
FileEntry entry = BundleManifest.AddEntry(targetType, file, relativePath, startOffset, compressedSize, Target.BundleMajorVersion);
|
||||
Tracer.Log($"Embed: {entry}");
|
||||
}
|
||||
}
|
||||
|
||||
// Write the bundle manifest
|
||||
headerOffset = BundleManifest.Write(writer);
|
||||
Tracer.Log($"Header Offset={headerOffset}");
|
||||
Tracer.Log($"Meta-data Size={writer.BaseStream.Position - headerOffset}");
|
||||
Tracer.Log($"Bundle: Path={bundlePath}, Size={bundle.Length}");
|
||||
}
|
||||
|
||||
HostWriter.SetAsBundle(bundlePath, headerOffset);
|
||||
|
||||
return bundlePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/Microsoft.NET.HostModel/Bundle/FileEntry.cs
Normal file
58
src/Microsoft.NET.HostModel/Bundle/FileEntry.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.NET.HostModel.Bundle
|
||||
{
|
||||
/// <summary>
|
||||
/// FileEntry: Records information about embedded files.
|
||||
///
|
||||
/// The bundle manifest records the following meta-data for each
|
||||
/// file embedded in the bundle:
|
||||
/// * Type (1 byte)
|
||||
/// * NameLength (7-bit extension encoding, typically 1 byte)
|
||||
/// * Name ("NameLength" Bytes)
|
||||
/// * Offset (Int64)
|
||||
/// * Size (Int64)
|
||||
/// === present only in bundle version 3+
|
||||
/// * CompressedSize (Int64) 0 indicates No Compression
|
||||
/// </summary>
|
||||
public class FileEntry
|
||||
{
|
||||
public readonly uint BundleMajorVersion;
|
||||
|
||||
public readonly long Offset;
|
||||
public readonly long Size;
|
||||
public readonly long CompressedSize;
|
||||
public readonly FileType Type;
|
||||
public readonly string RelativePath; // Path of an embedded file, relative to the Bundle source-directory.
|
||||
|
||||
public const char DirectorySeparatorChar = '/';
|
||||
|
||||
public FileEntry(FileType fileType, string relativePath, long offset, long size, long compressedSize, uint bundleMajorVersion)
|
||||
{
|
||||
BundleMajorVersion = bundleMajorVersion;
|
||||
Type = fileType;
|
||||
RelativePath = relativePath.Replace('\\', DirectorySeparatorChar);
|
||||
Offset = offset;
|
||||
Size = size;
|
||||
CompressedSize = compressedSize;
|
||||
}
|
||||
|
||||
public void Write(BinaryWriter writer)
|
||||
{
|
||||
writer.Write(Offset);
|
||||
writer.Write(Size);
|
||||
// compression is used only in version 6.0+
|
||||
if (BundleMajorVersion >= 6)
|
||||
{
|
||||
writer.Write(CompressedSize);
|
||||
}
|
||||
writer.Write((byte)Type);
|
||||
writer.Write(RelativePath);
|
||||
}
|
||||
|
||||
public override string ToString() => $"{RelativePath} [{Type}] @{Offset} Sz={Size} CompressedSz={CompressedSize}";
|
||||
}
|
||||
}
|
||||
36
src/Microsoft.NET.HostModel/Bundle/FileSpec.cs
Normal file
36
src/Microsoft.NET.HostModel/Bundle/FileSpec.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.NET.HostModel.Bundle
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about files to embed into the Bundle (input to the Bundler).
|
||||
///
|
||||
/// SourcePath: path to the file to be bundled at compile time
|
||||
/// BundleRelativePath: path where the file is expected at run time,
|
||||
/// relative to the app DLL.
|
||||
/// </summary>
|
||||
public class FileSpec
|
||||
{
|
||||
public readonly string SourcePath;
|
||||
public readonly string BundleRelativePath;
|
||||
public bool Excluded;
|
||||
|
||||
public FileSpec(string sourcePath, string bundleRelativePath)
|
||||
{
|
||||
SourcePath = sourcePath;
|
||||
BundleRelativePath = bundleRelativePath;
|
||||
Excluded = false;
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(SourcePath) &&
|
||||
!string.IsNullOrWhiteSpace(BundleRelativePath);
|
||||
}
|
||||
|
||||
public override string ToString() => $"SourcePath: {SourcePath}, RelativePath: {BundleRelativePath} {(Excluded ? "[Excluded]" : "")}";
|
||||
}
|
||||
}
|
||||
21
src/Microsoft.NET.HostModel/Bundle/FileType.cs
Normal file
21
src/Microsoft.NET.HostModel/Bundle/FileType.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.NET.HostModel.Bundle
|
||||
{
|
||||
/// <summary>
|
||||
/// FileType: Identifies the type of file embedded into the bundle.
|
||||
///
|
||||
/// The bundler differentiates a few kinds of files via the manifest,
|
||||
/// with respect to the way in which they'll be used by the runtime.
|
||||
/// </summary>
|
||||
public enum FileType : byte
|
||||
{
|
||||
Unknown, // Type not determined.
|
||||
Assembly, // IL and R2R Assemblies
|
||||
NativeBinary, // NativeBinaries
|
||||
DepsJson, // .deps.json configuration file
|
||||
RuntimeConfigJson, // .runtimeconfig.json configuration file
|
||||
Symbols // PDB Files
|
||||
};
|
||||
}
|
||||
177
src/Microsoft.NET.HostModel/Bundle/Manifest.cs
Normal file
177
src/Microsoft.NET.HostModel/Bundle/Manifest.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Microsoft.NET.HostModel.Bundle
|
||||
{
|
||||
/// <summary>
|
||||
/// BundleManifest is a description of the contents of a bundle file.
|
||||
/// This class handles creation and consumption of bundle-manifests.
|
||||
///
|
||||
/// Here is the description of the Bundle Layout:
|
||||
/// _______________________________________________
|
||||
/// AppHost
|
||||
///
|
||||
///
|
||||
/// ------------Embedded Files ---------------------
|
||||
/// The embedded files including the app, its
|
||||
/// configuration files, dependencies, and
|
||||
/// possibly the runtime.
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
/// ------------ Bundle Header -------------
|
||||
/// MajorVersion
|
||||
/// MinorVersion
|
||||
/// NumEmbeddedFiles
|
||||
/// ExtractionID
|
||||
/// DepsJson Location [Version 2+]
|
||||
/// Offset
|
||||
/// Size
|
||||
/// RuntimeConfigJson Location [Version 2+]
|
||||
/// Offset
|
||||
/// Size
|
||||
/// Flags [Version 2+]
|
||||
/// - - - - - - Manifest Entries - - - - - - - - - - -
|
||||
/// Series of FileEntries (for each embedded file)
|
||||
/// [File Type, Name, Offset, Size information]
|
||||
///
|
||||
///
|
||||
///
|
||||
/// _________________________________________________
|
||||
/// </summary>
|
||||
public class Manifest
|
||||
{
|
||||
// NetcoreApp3CompatMode flag is set on a .net5 app,
|
||||
// which chooses to build single-file apps in .netcore3.x compat mode,
|
||||
// by constructing the bundler with BundleAllConent option.
|
||||
// This mode is expected to be deprecated in future versions of .NET.
|
||||
[Flags]
|
||||
private enum HeaderFlags : ulong
|
||||
{
|
||||
None = 0,
|
||||
NetcoreApp3CompatMode = 1
|
||||
}
|
||||
|
||||
// Bundle ID is a string that is used to uniquely
|
||||
// identify this bundle. It is choosen to be compatible
|
||||
// with path-names so that the AppHost can use it in
|
||||
// extraction path.
|
||||
public string BundleID { get; private set; }
|
||||
//Same as Path.GetRandomFileName
|
||||
private const int BundleIdLength = 12;
|
||||
private SHA256 bundleHash = SHA256.Create();
|
||||
public readonly uint BundleMajorVersion;
|
||||
// The Minor version is currently unused, and is always zero
|
||||
public const uint BundleMinorVersion = 0;
|
||||
private FileEntry DepsJsonEntry;
|
||||
private FileEntry RuntimeConfigJsonEntry;
|
||||
private HeaderFlags Flags;
|
||||
public List<FileEntry> Files;
|
||||
public string BundleVersion => $"{BundleMajorVersion}.{BundleMinorVersion}";
|
||||
|
||||
public Manifest(uint bundleMajorVersion, bool netcoreapp3CompatMode = false)
|
||||
{
|
||||
BundleMajorVersion = bundleMajorVersion;
|
||||
Files = new List<FileEntry>();
|
||||
Flags = (netcoreapp3CompatMode) ? HeaderFlags.NetcoreApp3CompatMode : HeaderFlags.None;
|
||||
}
|
||||
|
||||
public FileEntry AddEntry(FileType type, FileStream fileContent, string relativePath, long offset, long compressedSize, uint bundleMajorVersion)
|
||||
{
|
||||
if (bundleHash == null)
|
||||
{
|
||||
throw new InvalidOperationException("It is forbidden to change Manifest state after it was written or BundleId was obtained.");
|
||||
}
|
||||
|
||||
FileEntry entry = new FileEntry(type, relativePath, offset, fileContent.Length, compressedSize, bundleMajorVersion);
|
||||
Files.Add(entry);
|
||||
|
||||
fileContent.Position = 0;
|
||||
byte[] hashBytes = ComputeSha256Hash(fileContent);
|
||||
bundleHash.TransformBlock(hashBytes, 0, hashBytes.Length, hashBytes, 0);
|
||||
|
||||
switch (entry.Type)
|
||||
{
|
||||
case FileType.DepsJson:
|
||||
DepsJsonEntry = entry;
|
||||
break;
|
||||
case FileType.RuntimeConfigJson:
|
||||
RuntimeConfigJsonEntry = entry;
|
||||
break;
|
||||
|
||||
case FileType.Assembly:
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static byte[] ComputeSha256Hash(Stream stream)
|
||||
{
|
||||
using (SHA256 sha = SHA256.Create())
|
||||
{
|
||||
return sha.ComputeHash(stream);
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateDeterministicId()
|
||||
{
|
||||
bundleHash.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
|
||||
byte[] manifestHash = bundleHash.Hash;
|
||||
bundleHash.Dispose();
|
||||
bundleHash = null;
|
||||
|
||||
return Convert.ToBase64String(manifestHash).Substring(BundleIdLength).Replace('/', '_');
|
||||
}
|
||||
|
||||
public long Write(BinaryWriter writer)
|
||||
{
|
||||
BundleID = BundleID ?? GenerateDeterministicId();
|
||||
|
||||
long startOffset = writer.BaseStream.Position;
|
||||
|
||||
// Write the bundle header
|
||||
writer.Write(BundleMajorVersion);
|
||||
writer.Write(BundleMinorVersion);
|
||||
writer.Write(Files.Count);
|
||||
writer.Write(BundleID);
|
||||
|
||||
if (BundleMajorVersion >= 2)
|
||||
{
|
||||
writer.Write((DepsJsonEntry != null) ? DepsJsonEntry.Offset : 0);
|
||||
writer.Write((DepsJsonEntry != null) ? DepsJsonEntry.Size : 0);
|
||||
|
||||
writer.Write((RuntimeConfigJsonEntry != null) ? RuntimeConfigJsonEntry.Offset : 0);
|
||||
writer.Write((RuntimeConfigJsonEntry != null) ? RuntimeConfigJsonEntry.Size : 0);
|
||||
|
||||
writer.Write((ulong)Flags);
|
||||
}
|
||||
|
||||
// Write the manifest entries
|
||||
foreach (FileEntry entry in Files)
|
||||
{
|
||||
entry.Write(writer);
|
||||
}
|
||||
|
||||
return startOffset;
|
||||
}
|
||||
|
||||
public bool Contains(string relativePath)
|
||||
{
|
||||
return Files.Any(entry => relativePath.Equals(entry.RelativePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
120
src/Microsoft.NET.HostModel/Bundle/TargetInfo.cs
Normal file
120
src/Microsoft.NET.HostModel/Bundle/TargetInfo.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.NET.HostModel.AppHost;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.NET.HostModel.Bundle
|
||||
{
|
||||
/// <summary>
|
||||
/// TargetInfo: Information about the target for which the single-file bundle is built.
|
||||
///
|
||||
/// Currently the TargetInfo only tracks:
|
||||
/// - the target operating system
|
||||
/// - the target architecture
|
||||
/// - the target framework
|
||||
/// - the default options for this target
|
||||
/// - the assembly alignment for this target
|
||||
/// </summary>
|
||||
|
||||
public class TargetInfo
|
||||
{
|
||||
public readonly OSPlatform OS;
|
||||
public readonly Architecture Arch;
|
||||
public readonly Version FrameworkVersion;
|
||||
public readonly uint BundleMajorVersion;
|
||||
public readonly BundleOptions DefaultOptions;
|
||||
public readonly int AssemblyAlignment;
|
||||
|
||||
public TargetInfo(OSPlatform? os, Architecture? arch, Version targetFrameworkVersion)
|
||||
{
|
||||
OS = os ?? HostOS;
|
||||
Arch = arch ?? RuntimeInformation.OSArchitecture;
|
||||
FrameworkVersion = targetFrameworkVersion ?? net60;
|
||||
|
||||
Debug.Assert(IsLinux || IsOSX || IsWindows);
|
||||
|
||||
if (FrameworkVersion.CompareTo(net60) >= 0)
|
||||
{
|
||||
BundleMajorVersion = 6u;
|
||||
DefaultOptions = BundleOptions.None;
|
||||
}
|
||||
else if (FrameworkVersion.CompareTo(net50) >= 0)
|
||||
{
|
||||
BundleMajorVersion = 2u;
|
||||
DefaultOptions = BundleOptions.None;
|
||||
}
|
||||
else if (FrameworkVersion.Major == 3 && (FrameworkVersion.Minor == 0 || FrameworkVersion.Minor == 1))
|
||||
{
|
||||
BundleMajorVersion = 1u;
|
||||
DefaultOptions = BundleOptions.BundleAllContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Invalid input: Unsupported Target Framework Version {targetFrameworkVersion}");
|
||||
}
|
||||
|
||||
if (IsLinux && Arch == Architecture.Arm64)
|
||||
{
|
||||
// We align assemblies in the bundle at 4K so that we can use mmap on Linux without changing the page alignment of ARM64 R2R code.
|
||||
// This is only necessary for R2R assemblies, but we do it for all assemblies for simplicity.
|
||||
// See https://github.com/dotnet/runtime/issues/41832.
|
||||
AssemblyAlignment = 4096;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, assemblies are 16 bytes aligned, so that their sections can be memory-mapped cache aligned.
|
||||
AssemblyAlignment = 16;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsNativeBinary(string filePath)
|
||||
{
|
||||
return IsLinux ? ElfUtils.IsElfImage(filePath) : IsOSX ? MachOUtils.IsMachOImage(filePath) : PEUtils.IsPEImage(filePath);
|
||||
}
|
||||
|
||||
public string GetAssemblyName(string hostName)
|
||||
{
|
||||
// This logic to calculate assembly name from hostName should be removed (and probably moved to test helpers)
|
||||
// once the SDK in the correct assembly name.
|
||||
return (IsWindows ? Path.GetFileNameWithoutExtension(hostName) : hostName);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string os = IsWindows ? "win" : IsLinux ? "linux" : "osx";
|
||||
string arch = Arch.ToString().ToLowerInvariant();
|
||||
return $"OS: {os} Arch: {arch} FrameworkVersion: {FrameworkVersion}";
|
||||
}
|
||||
|
||||
private static OSPlatform HostOS => RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? OSPlatform.Linux :
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? OSPlatform.OSX : OSPlatform.Windows;
|
||||
|
||||
public bool IsLinux => OS.Equals(OSPlatform.Linux);
|
||||
public bool IsOSX => OS.Equals(OSPlatform.OSX);
|
||||
public bool IsWindows => OS.Equals(OSPlatform.Windows);
|
||||
|
||||
// The .net core 3 apphost doesn't care about semantics of FileType -- all files are extracted at startup.
|
||||
// However, the apphost checks that the FileType value is within expected bounds, so set it to the first enumeration.
|
||||
public FileType TargetSpecificFileType(FileType fileType) => (BundleMajorVersion == 1) ? FileType.Unknown : fileType;
|
||||
|
||||
// In .net core 3.x, bundle processing happens within the AppHost.
|
||||
// Therefore HostFxr and HostPolicy can be bundled within the single-file app.
|
||||
// In .net 5, bundle processing happens in HostFxr and HostPolicy libraries.
|
||||
// Therefore, these libraries themselves cannot be bundled into the single-file app.
|
||||
// This problem is mitigated by statically linking these host components with the AppHost.
|
||||
// https://github.com/dotnet/runtime/issues/32823
|
||||
public bool ShouldExclude(string relativePath) =>
|
||||
(FrameworkVersion.Major != 3) && (relativePath.Equals(HostFxr) || relativePath.Equals(HostPolicy));
|
||||
|
||||
private readonly Version net60 = new Version(6, 0);
|
||||
private readonly Version net50 = new Version(5, 0);
|
||||
private string HostFxr => IsWindows ? "hostfxr.dll" : IsLinux ? "libhostfxr.so" : "libhostfxr.dylib";
|
||||
private string HostPolicy => IsWindows ? "hostpolicy.dll" : IsLinux ? "libhostpolicy.so" : "libhostpolicy.dylib";
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
33
src/Microsoft.NET.HostModel/Bundle/Trace.cs
Normal file
33
src/Microsoft.NET.HostModel/Bundle/Trace.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.NET.HostModel.Bundle
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracing utilities for diagnostic output
|
||||
/// </summary>
|
||||
public class Trace
|
||||
{
|
||||
private readonly bool Verbose;
|
||||
|
||||
public Trace(bool verbose)
|
||||
{
|
||||
Verbose = verbose;
|
||||
}
|
||||
|
||||
public void Log(string fmt, params object[] args)
|
||||
{
|
||||
if (Verbose)
|
||||
{
|
||||
Console.WriteLine("LOG: " + fmt, args);
|
||||
}
|
||||
}
|
||||
|
||||
public void Error(string type, string message)
|
||||
{
|
||||
Console.Error.WriteLine($"ERROR: {message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
304
src/Microsoft.NET.HostModel/ComHost/ClsidMap.cs
Normal file
304
src/Microsoft.NET.HostModel/ComHost/ClsidMap.cs
Normal file
@@ -0,0 +1,304 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.NET.HostModel.ComHost
|
||||
{
|
||||
public static class ClsidMap
|
||||
{
|
||||
private struct ClsidEntry
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; }
|
||||
[JsonPropertyName("assembly")]
|
||||
public string Assembly { get; set; }
|
||||
[JsonPropertyName("progid")]
|
||||
public string ProgId { get; set; }
|
||||
}
|
||||
|
||||
public static void Create(MetadataReader metadataReader, string clsidMapPath)
|
||||
{
|
||||
Dictionary<string, ClsidEntry> clsidMap = new Dictionary<string, ClsidEntry>();
|
||||
|
||||
string assemblyName = GetAssemblyName(metadataReader).FullName;
|
||||
|
||||
bool isAssemblyComVisible = IsComVisible(metadataReader, metadataReader.GetAssemblyDefinition());
|
||||
|
||||
foreach (TypeDefinitionHandle type in metadataReader.TypeDefinitions)
|
||||
{
|
||||
TypeDefinition definition = metadataReader.GetTypeDefinition(type);
|
||||
|
||||
// Only public COM-visible classes can be exposed via the COM host.
|
||||
if (TypeIsPublic(metadataReader, definition) && TypeIsClass(metadataReader, definition) && IsComVisible(metadataReader, definition, isAssemblyComVisible))
|
||||
{
|
||||
Guid guid = GetTypeGuid(metadataReader, definition);
|
||||
string guidString = GetTypeGuid(metadataReader, definition).ToString("B");
|
||||
|
||||
if (clsidMap.ContainsKey(guidString))
|
||||
{
|
||||
throw new ConflictingGuidException(clsidMap[guidString].Type, GetTypeName(metadataReader, definition), guid);
|
||||
}
|
||||
|
||||
string progId = GetProgId(metadataReader, definition);
|
||||
|
||||
clsidMap.Add(guidString,
|
||||
new ClsidEntry
|
||||
{
|
||||
Type = GetTypeName(metadataReader, definition),
|
||||
Assembly = assemblyName,
|
||||
ProgId = !string.IsNullOrWhiteSpace(progId) ? progId : null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
using (StreamWriter writer = File.CreateText(clsidMapPath))
|
||||
{
|
||||
writer.Write(JsonSerializer.Serialize(clsidMap, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TypeIsClass(MetadataReader metadataReader, TypeDefinition definition)
|
||||
{
|
||||
if ((definition.Attributes & TypeAttributes.Interface) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EntityHandle baseTypeEntity = definition.BaseType;
|
||||
if (baseTypeEntity.Kind == HandleKind.TypeReference)
|
||||
{
|
||||
TypeReference baseClass = metadataReader.GetTypeReference((TypeReferenceHandle)baseTypeEntity);
|
||||
if (baseClass.ResolutionScope.Kind == HandleKind.AssemblyReference)
|
||||
{
|
||||
if (HasTypeName(metadataReader, baseClass, "System", "ValueType") || HasTypeName(metadataReader, baseClass, "System", "Enum"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TypeIsPublic(MetadataReader reader, TypeDefinition type)
|
||||
{
|
||||
switch (type.Attributes & TypeAttributes.VisibilityMask)
|
||||
{
|
||||
case TypeAttributes.Public:
|
||||
return true;
|
||||
case TypeAttributes.NestedPublic:
|
||||
return TypeIsPublic(reader, reader.GetTypeDefinition(type.GetDeclaringType()));
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetTypeName(MetadataReader metadataReader, TypeDefinition type)
|
||||
{
|
||||
if (!type.GetDeclaringType().IsNil)
|
||||
{
|
||||
return $"{GetTypeName(metadataReader, metadataReader.GetTypeDefinition(type.GetDeclaringType()))}+{metadataReader.GetString(type.Name)}";
|
||||
}
|
||||
return $"{metadataReader.GetString(type.Namespace)}{Type.Delimiter}{metadataReader.GetString(type.Name)}";
|
||||
}
|
||||
|
||||
private static bool HasTypeName(MetadataReader metadataReader, TypeReference type, string ns, string name)
|
||||
{
|
||||
return metadataReader.StringComparer.Equals(type.Namespace, ns) && metadataReader.StringComparer.Equals(type.Name, name);
|
||||
}
|
||||
|
||||
private static AssemblyName GetAssemblyName(MetadataReader metadataReader)
|
||||
{
|
||||
AssemblyName name = new AssemblyName();
|
||||
|
||||
AssemblyDefinition definition = metadataReader.GetAssemblyDefinition();
|
||||
name.Name = metadataReader.GetString(definition.Name);
|
||||
name.Version = definition.Version;
|
||||
name.CultureInfo = CultureInfo.GetCultureInfo(metadataReader.GetString(definition.Culture));
|
||||
name.SetPublicKey(metadataReader.GetBlobBytes(definition.PublicKey));
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private static bool IsComVisible(MetadataReader reader, AssemblyDefinition assembly)
|
||||
{
|
||||
CustomAttributeHandle handle = GetComVisibleAttribute(reader, assembly.GetCustomAttributes());
|
||||
|
||||
if (handle.IsNil)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CustomAttribute comVisibleAttribute = reader.GetCustomAttribute(handle);
|
||||
CustomAttributeValue<KnownType> data = comVisibleAttribute.DecodeValue(new TypeResolver());
|
||||
return (bool)data.FixedArguments[0].Value;
|
||||
}
|
||||
|
||||
private static bool IsComVisible(MetadataReader metadataReader, TypeDefinition definition, bool assemblyComVisible)
|
||||
{
|
||||
// We need to ensure that all parent scopes of the given type are not explicitly non-ComVisible.
|
||||
bool? IsComVisibleCore(TypeDefinition typeDefinition)
|
||||
{
|
||||
CustomAttributeHandle handle = GetComVisibleAttribute(metadataReader, typeDefinition.GetCustomAttributes());
|
||||
if (handle.IsNil)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
CustomAttribute comVisibleAttribute = metadataReader.GetCustomAttribute(handle);
|
||||
CustomAttributeValue<KnownType> data = comVisibleAttribute.DecodeValue(new TypeResolver());
|
||||
return (bool)data.FixedArguments[0].Value;
|
||||
}
|
||||
|
||||
if (!definition.GetDeclaringType().IsNil)
|
||||
{
|
||||
return IsComVisible(metadataReader, metadataReader.GetTypeDefinition(definition.GetDeclaringType()), assemblyComVisible) && (IsComVisibleCore(definition) ?? assemblyComVisible);
|
||||
}
|
||||
|
||||
return IsComVisibleCore(definition) ?? assemblyComVisible;
|
||||
}
|
||||
|
||||
private static CustomAttributeHandle GetComVisibleAttribute(MetadataReader reader, CustomAttributeHandleCollection customAttributes)
|
||||
{
|
||||
foreach (CustomAttributeHandle attr in customAttributes)
|
||||
{
|
||||
CustomAttribute attribute = reader.GetCustomAttribute(attr);
|
||||
if (IsTargetAttribute(reader, attribute, "System.Runtime.InteropServices", "ComVisibleAttribute"))
|
||||
{
|
||||
return attr;
|
||||
}
|
||||
}
|
||||
return default;
|
||||
}
|
||||
|
||||
private static Guid GetTypeGuid(MetadataReader reader, TypeDefinition type)
|
||||
{
|
||||
// Find the class' GUID by reading the GuidAttribute value.
|
||||
// We do not support implicit runtime-generated GUIDs for the .NET Core COM host.
|
||||
foreach (CustomAttributeHandle attr in type.GetCustomAttributes())
|
||||
{
|
||||
CustomAttribute attribute = reader.GetCustomAttribute(attr);
|
||||
if (IsTargetAttribute(reader, attribute, "System.Runtime.InteropServices", "GuidAttribute"))
|
||||
{
|
||||
CustomAttributeValue<KnownType> data = attribute.DecodeValue(new TypeResolver());
|
||||
return Guid.Parse((string)data.FixedArguments[0].Value);
|
||||
}
|
||||
}
|
||||
throw new MissingGuidException(GetTypeName(reader, type));
|
||||
}
|
||||
|
||||
private static string GetProgId(MetadataReader reader, TypeDefinition type)
|
||||
{
|
||||
foreach (CustomAttributeHandle attr in type.GetCustomAttributes())
|
||||
{
|
||||
CustomAttribute attribute = reader.GetCustomAttribute(attr);
|
||||
if (IsTargetAttribute(reader, attribute, "System.Runtime.InteropServices", "ProgIdAttribute"))
|
||||
{
|
||||
CustomAttributeValue<KnownType> data = attribute.DecodeValue(new TypeResolver());
|
||||
return (string)data.FixedArguments[0].Value;
|
||||
}
|
||||
}
|
||||
return GetTypeName(reader, type);
|
||||
}
|
||||
|
||||
private static bool IsTargetAttribute(MetadataReader reader, CustomAttribute attribute, string targetNamespace, string targetName)
|
||||
{
|
||||
StringHandle namespaceMaybe;
|
||||
StringHandle nameMaybe;
|
||||
switch (attribute.Constructor.Kind)
|
||||
{
|
||||
case HandleKind.MemberReference:
|
||||
MemberReference refConstructor = reader.GetMemberReference((MemberReferenceHandle)attribute.Constructor);
|
||||
TypeReference refType = reader.GetTypeReference((TypeReferenceHandle)refConstructor.Parent);
|
||||
namespaceMaybe = refType.Namespace;
|
||||
nameMaybe = refType.Name;
|
||||
break;
|
||||
|
||||
case HandleKind.MethodDefinition:
|
||||
MethodDefinition defConstructor = reader.GetMethodDefinition((MethodDefinitionHandle)attribute.Constructor);
|
||||
TypeDefinition defType = reader.GetTypeDefinition(defConstructor.GetDeclaringType());
|
||||
namespaceMaybe = defType.Namespace;
|
||||
nameMaybe = defType.Name;
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.Assert(false, "Unknown attribute constructor kind");
|
||||
return false;
|
||||
}
|
||||
|
||||
return reader.StringComparer.Equals(namespaceMaybe, targetNamespace) && reader.StringComparer.Equals(nameMaybe, targetName);
|
||||
}
|
||||
|
||||
private enum KnownType
|
||||
{
|
||||
Bool,
|
||||
String,
|
||||
SystemType,
|
||||
Unknown
|
||||
}
|
||||
|
||||
private class TypeResolver : ICustomAttributeTypeProvider<KnownType>
|
||||
{
|
||||
public KnownType GetPrimitiveType(PrimitiveTypeCode typeCode)
|
||||
{
|
||||
switch (typeCode)
|
||||
{
|
||||
case PrimitiveTypeCode.Boolean:
|
||||
return KnownType.Bool;
|
||||
case PrimitiveTypeCode.String:
|
||||
return KnownType.String;
|
||||
default:
|
||||
return KnownType.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
public KnownType GetSystemType()
|
||||
{
|
||||
return KnownType.SystemType;
|
||||
}
|
||||
|
||||
public KnownType GetSZArrayType(KnownType elementType)
|
||||
{
|
||||
return KnownType.Unknown;
|
||||
}
|
||||
|
||||
public KnownType GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind)
|
||||
{
|
||||
return KnownType.Unknown;
|
||||
}
|
||||
|
||||
public KnownType GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind)
|
||||
{
|
||||
return KnownType.Unknown;
|
||||
}
|
||||
|
||||
public KnownType GetTypeFromSerializedName(string name)
|
||||
{
|
||||
return KnownType.Unknown;
|
||||
}
|
||||
|
||||
public PrimitiveTypeCode GetUnderlyingEnumType(KnownType type)
|
||||
{
|
||||
throw new BadImageFormatException("Unexpectedly got an enum parameter for an attribute.");
|
||||
}
|
||||
|
||||
public bool IsSystemType(KnownType type)
|
||||
{
|
||||
return type == KnownType.SystemType;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
80
src/Microsoft.NET.HostModel/ComHost/ComHost.cs
Normal file
80
src/Microsoft.NET.HostModel/ComHost/ComHost.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.NET.HostModel.ComHost
|
||||
{
|
||||
public class ComHost
|
||||
{
|
||||
private const int E_INVALIDARG = unchecked((int)0x80070057);
|
||||
// These need to match RESOURCEID_CLSIDMAP and RESOURCETYPE_CLSIDMAP defined in comhost.h.
|
||||
private const int ClsidmapResourceId = 64;
|
||||
private const int ClsidmapResourceType = 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Create a ComHost with an embedded CLSIDMap file to map CLSIDs to .NET Classes.
|
||||
/// </summary>
|
||||
/// <param name="comHostSourceFilePath">The path of Apphost template, which has the place holder</param>
|
||||
/// <param name="comHostDestinationFilePath">The destination path for desired location to place, including the file name</param>
|
||||
/// <param name="clsidmapFilePath">The path to the *.clsidmap file.</param>
|
||||
/// <param name="typeLibraries">Resource ids for tlbs and paths to the tlb files to be embedded.</param>
|
||||
public static void Create(
|
||||
string comHostSourceFilePath,
|
||||
string comHostDestinationFilePath,
|
||||
string clsidmapFilePath,
|
||||
IReadOnlyDictionary<int, string> typeLibraries = null)
|
||||
{
|
||||
var destinationDirectory = new FileInfo(comHostDestinationFilePath).Directory.FullName;
|
||||
if (!Directory.Exists(destinationDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(destinationDirectory);
|
||||
}
|
||||
|
||||
// Copy apphost to destination path so it inherits the same attributes/permissions.
|
||||
File.Copy(comHostSourceFilePath, comHostDestinationFilePath, overwrite: true);
|
||||
|
||||
if (!ResourceUpdater.IsSupportedOS())
|
||||
{
|
||||
throw new ComHostCustomizationUnsupportedOSException();
|
||||
}
|
||||
|
||||
string clsidMap = File.ReadAllText(clsidmapFilePath);
|
||||
byte[] clsidMapBytes = Encoding.UTF8.GetBytes(clsidMap);
|
||||
|
||||
using (ResourceUpdater updater = new ResourceUpdater(comHostDestinationFilePath))
|
||||
{
|
||||
updater.AddResource(clsidMapBytes, (IntPtr)ClsidmapResourceType, (IntPtr)ClsidmapResourceId);
|
||||
if (typeLibraries is not null)
|
||||
{
|
||||
foreach (var typeLibrary in typeLibraries)
|
||||
{
|
||||
if (!ResourceUpdater.IsIntResource((IntPtr)typeLibrary.Key))
|
||||
{
|
||||
throw new InvalidTypeLibraryIdException(typeLibrary.Value, typeLibrary.Key);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
byte[] tlbFileBytes = File.ReadAllBytes(typeLibrary.Value);
|
||||
updater.AddResource(tlbFileBytes, "typelib", (IntPtr)typeLibrary.Key);
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
throw new TypeLibraryDoesNotExistException(typeLibrary.Value, ex);
|
||||
}
|
||||
catch (HResultException hr) when (hr.Win32HResult == E_INVALIDARG)
|
||||
{
|
||||
throw new InvalidTypeLibraryException(typeLibrary.Value, hr);
|
||||
}
|
||||
}
|
||||
}
|
||||
updater.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.NET.HostModel.ComHost
|
||||
{
|
||||
/// <summary>
|
||||
/// The application host executable cannot be customized because adding resources requires
|
||||
/// that the build be performed on Windows (excluding Nano Server).
|
||||
/// </summary>
|
||||
public class ComHostCustomizationUnsupportedOSException : Exception
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.NET.HostModel.ComHost
|
||||
{
|
||||
/// <summary>
|
||||
/// The same Guid has been specified for two public ComVisible classes in the assembly.
|
||||
/// </summary>
|
||||
public class ConflictingGuidException : Exception
|
||||
{
|
||||
public ConflictingGuidException(string typeName1, string typeName2, Guid guid)
|
||||
{
|
||||
if (typeName1 is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(typeName1));
|
||||
}
|
||||
if (typeName2 is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(typeName2));
|
||||
}
|
||||
TypeName1 = typeName1;
|
||||
TypeName2 = typeName2;
|
||||
Guid = guid;
|
||||
}
|
||||
|
||||
public string TypeName1 { get; }
|
||||
public string TypeName2 { get; }
|
||||
public Guid Guid { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.NET.HostModel.ComHost
|
||||
{
|
||||
/// <summary>
|
||||
/// The provided type library file is an invalid format.
|
||||
/// </summary>
|
||||
public class InvalidTypeLibraryException : Exception
|
||||
{
|
||||
public InvalidTypeLibraryException(string path)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
|
||||
public InvalidTypeLibraryException(string path, Exception innerException)
|
||||
:base($"Invalid type library at '{path}'.", innerException)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.NET.HostModel.ComHost
|
||||
{
|
||||
/// <summary>
|
||||
/// The provided resource id for the type library is unsupported.
|
||||
/// </summary>
|
||||
public class InvalidTypeLibraryIdException : Exception
|
||||
{
|
||||
public InvalidTypeLibraryIdException(string path, int id)
|
||||
{
|
||||
Path = path;
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public int Id { get; }
|
||||
}
|
||||
}
|
||||
26
src/Microsoft.NET.HostModel/ComHost/MissingGuidException.cs
Normal file
26
src/Microsoft.NET.HostModel/ComHost/MissingGuidException.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.NET.HostModel.ComHost
|
||||
{
|
||||
/// <summary>
|
||||
/// The type <see cref="TypeName"/> is public and ComVisible but does not have a <see cref="System.Runtime.InteropServices.GuidAttribute"/> attribute.
|
||||
/// </summary>
|
||||
public class MissingGuidException : Exception
|
||||
{
|
||||
public MissingGuidException(string typeName)
|
||||
{
|
||||
if (typeName is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(typeName));
|
||||
}
|
||||
TypeName = typeName;
|
||||
}
|
||||
|
||||
public string TypeName { get; }
|
||||
}
|
||||
}
|
||||
104
src/Microsoft.NET.HostModel/ComHost/RegFreeComManifest.cs
Normal file
104
src/Microsoft.NET.HostModel/ComHost/RegFreeComManifest.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Microsoft.NET.HostModel.ComHost
|
||||
{
|
||||
public class RegFreeComManifest
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a side-by-side application manifest to enable reg-free COM.
|
||||
/// </summary>
|
||||
/// <param name="assemblyName">The name of the assembly.</param>
|
||||
/// <param name="comHostName">The name of the comhost library.</param>
|
||||
/// <param name="assemblyVersion">The version of the assembly.</param>
|
||||
/// <param name="clsidMapPath">The path to the clsidmap file.</param>
|
||||
/// <param name="comManifestPath">The path to which to write the manifest.</param>
|
||||
/// <param name="typeLibraries">The type libraries to include in the manifest.</param>
|
||||
public static void CreateManifestFromClsidmap(string assemblyName, string comHostName, string assemblyVersion, string clsidMapPath, string comManifestPath, IReadOnlyDictionary<int, string> typeLibraries = null)
|
||||
{
|
||||
XNamespace ns = "urn:schemas-microsoft-com:asm.v1";
|
||||
|
||||
XElement manifest = new XElement(ns + "assembly", new XAttribute("manifestVersion", "1.0"));
|
||||
manifest.Add(new XElement(ns + "assemblyIdentity",
|
||||
new XAttribute("type", "win32"),
|
||||
new XAttribute("name", $"{assemblyName}.X"),
|
||||
new XAttribute("version", assemblyVersion)));
|
||||
|
||||
var fileElement = CreateComHostFileElement(clsidMapPath, comHostName, ns);
|
||||
if (typeLibraries is not null)
|
||||
{
|
||||
AddTypeLibElementsToFileElement(typeLibraries, ns, fileElement);
|
||||
}
|
||||
manifest.Add(fileElement);
|
||||
|
||||
XDocument manifestDocument = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"), manifest);
|
||||
XmlWriterSettings settings = new XmlWriterSettings()
|
||||
{
|
||||
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)
|
||||
};
|
||||
using (XmlWriter manifestWriter = XmlWriter.Create(comManifestPath, settings))
|
||||
{
|
||||
manifestDocument.WriteTo(manifestWriter);
|
||||
}
|
||||
}
|
||||
|
||||
private static XElement CreateComHostFileElement(string clsidMapPath, string comHostName, XNamespace ns)
|
||||
{
|
||||
XElement fileElement = new XElement(ns + "file", new XAttribute("name", comHostName));
|
||||
JsonElement clsidMap;
|
||||
using (FileStream clsidMapStream = File.OpenRead(clsidMapPath))
|
||||
{
|
||||
clsidMap = JsonDocument.Parse(clsidMapStream).RootElement;
|
||||
}
|
||||
|
||||
foreach (JsonProperty property in clsidMap.EnumerateObject())
|
||||
{
|
||||
string guidMaybe = property.Name;
|
||||
Guid guid = Guid.Parse(guidMaybe);
|
||||
XElement comClassElement = new XElement(ns + "comClass", new XAttribute("clsid", guid.ToString("B")), new XAttribute("threadingModel", "Both"));
|
||||
if (property.Value.TryGetProperty("progid", out JsonElement progIdValue))
|
||||
{
|
||||
comClassElement.Add(new XAttribute("progid", progIdValue.GetString()));
|
||||
}
|
||||
|
||||
fileElement.Add(comClassElement);
|
||||
}
|
||||
return fileElement;
|
||||
}
|
||||
|
||||
private static void AddTypeLibElementsToFileElement(IReadOnlyDictionary<int, string> typeLibraries, XNamespace ns, XElement fileElement)
|
||||
{
|
||||
foreach (var typeLibrary in typeLibraries)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] tlbFileBytes = File.ReadAllBytes(typeLibrary.Value);
|
||||
TypeLibReader reader = new TypeLibReader(tlbFileBytes);
|
||||
if (!reader.TryReadTypeLibGuidAndVersion(out Guid name, out Version version))
|
||||
{
|
||||
throw new InvalidTypeLibraryException(typeLibrary.Value);
|
||||
}
|
||||
XElement typeLibElement = new XElement(ns + "typelib",
|
||||
new XAttribute("tlbid", name.ToString("B")),
|
||||
new XAttribute("resourceid", typeLibrary.Key),
|
||||
new XAttribute("version", version),
|
||||
new XAttribute("helpdir", ""));
|
||||
fileElement.Add(typeLibElement);
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
throw new TypeLibraryDoesNotExistException(typeLibrary.Value, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/Microsoft.NET.HostModel/ComHost/TypeLibReader.cs
Normal file
75
src/Microsoft.NET.HostModel/ComHost/TypeLibReader.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace Microsoft.NET.HostModel.ComHost
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads data from a COM Type Library file based on the official implementation in the Win32 function LoadTypeLib.
|
||||
/// We do the reading ourselves instead of calling into the OS so we don't have to worry about the type library's
|
||||
/// dependencies being discoverable on disk.
|
||||
/// </summary>
|
||||
internal class TypeLibReader
|
||||
{
|
||||
private byte[] tlbBytes;
|
||||
|
||||
public TypeLibReader(byte[] tlbBytes)
|
||||
{
|
||||
this.tlbBytes = tlbBytes;
|
||||
}
|
||||
|
||||
private const int OffsetOfGuidOffset = sizeof(int) * 2;
|
||||
private const int SizeOfGuidOffset = sizeof(int);
|
||||
|
||||
private const int OffsetOfMajorVersion = OffsetOfGuidOffset + SizeOfGuidOffset + sizeof(int) * 2 + sizeof(ushort) * 2;
|
||||
private const int SizeOfMajorVersion = sizeof(ushort);
|
||||
private const int OffsetOfMinorVersion = OffsetOfMajorVersion + SizeOfMajorVersion;
|
||||
private const int SizeOfMinorVersion = sizeof(ushort);
|
||||
|
||||
private const int OffsetOfTypeInfosCount = OffsetOfMinorVersion + SizeOfMinorVersion + sizeof(int);
|
||||
private const int SizeOfTypeInfosCount = sizeof(int);
|
||||
|
||||
private const int OffsetOfTablesStart = OffsetOfTypeInfosCount + SizeOfTypeInfosCount + sizeof(int) * 12;
|
||||
private const int NumTablesToSkip = 5;
|
||||
private const int SizeOfTableHeader = sizeof(int) * 4;
|
||||
|
||||
private Guid FindGuid(ReadOnlySpan<byte> fileContents)
|
||||
{
|
||||
checked
|
||||
{
|
||||
int typelibGuidEntryOffset = (int)BinaryPrimitives.ReadUInt32LittleEndian(fileContents.Slice(OffsetOfGuidOffset));
|
||||
int infoRefsOffsetCount = (int)BinaryPrimitives.ReadUInt32LittleEndian(fileContents.Slice(OffsetOfTypeInfosCount));
|
||||
int infoBytes = infoRefsOffsetCount * SizeOfTypeInfosCount;
|
||||
int guidTableOffset = OffsetOfTablesStart + infoBytes + SizeOfTableHeader * NumTablesToSkip;
|
||||
int fileOffset = (int)BinaryPrimitives.ReadUInt32LittleEndian(fileContents.Slice(guidTableOffset));
|
||||
return new Guid(fileContents.Slice(fileOffset + typelibGuidEntryOffset, 16).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryReadTypeLibGuidAndVersion(out Guid typelibId, out Version version)
|
||||
{
|
||||
typelibId = default;
|
||||
version = default;
|
||||
try
|
||||
{
|
||||
var span = new ReadOnlySpan<byte>(tlbBytes);
|
||||
typelibId = FindGuid(span);
|
||||
ushort majorVer = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(OffsetOfMajorVersion));
|
||||
ushort minorVer = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(OffsetOfMinorVersion));
|
||||
version = new Version(majorVer, minorVer);
|
||||
return true;
|
||||
}
|
||||
catch (System.OverflowException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (System.IndexOutOfRangeException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.NET.HostModel.ComHost
|
||||
{
|
||||
/// <summary>
|
||||
/// The specified type library path does not exist.
|
||||
/// </summary>
|
||||
public class TypeLibraryDoesNotExistException : Exception
|
||||
{
|
||||
public TypeLibraryDoesNotExistException(string path, Exception innerException)
|
||||
:base($"Type library '{path}' does not exist.", innerException)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
}
|
||||
}
|
||||
29
src/Microsoft.NET.HostModel/Microsoft.NET.HostModel.csproj
Normal file
29
src/Microsoft.NET.HostModel/Microsoft.NET.HostModel.csproj
Normal file
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<Description>Abstractions for modifying .NET host binaries</Description>
|
||||
<IsShipping>false</IsShipping>
|
||||
<IsPackable>true</IsPackable>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<Serviceable>true</Serviceable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<!-- Managed API isn't completely documented yet. TODO: https://github.com/dotnet/core-setup/issues/5108 -->
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<!-- Historically, the key for the managed projects is the AspNetCore key Arcade carries. -->
|
||||
<StrongNameKeyId>MicrosoftAspNetCore</StrongNameKeyId>
|
||||
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
|
||||
<PackageId Condition="'$(PgoInstrument)' == 'true'">Microsoft.Net.HostModel.PGO</PackageId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Reflection.Metadata" Version="1.8.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="$(SystemTextJsonVersion)" />
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="$(SystemRuntimeCompilerServicesUnsafeVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
19
src/Microsoft.NET.HostModel/README.md
Normal file
19
src/Microsoft.NET.HostModel/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
Host Model
|
||||
===================================
|
||||
|
||||
HostModel is a library used by the [SDK](https://github.com/dotnet/sdk) to perform certain transformations on host executables. The main services implemented in HostModel are:
|
||||
|
||||
* AppHost rewriter: Embeds the App Name into the AppHost executable. On Windows, also copies resources from App.dll to the AppHost.
|
||||
* ComHost rewriter: Creates a ComHost with an embedded CLSIDMap file to map CLSIDs to .NET Classes.
|
||||
|
||||
* Single-file bundler: Embeds an application and its dependencies into the AppHost, to publish a single executable, as described [here](https://github.com/dotnet/designs/blob/master/accepted/2020/single-file/design.md).
|
||||
|
||||
The HostModel library is in the Runtime repo because:
|
||||
|
||||
* The implementations of the host and HostModel are closely related, which facilitates easy development, update, and testing.
|
||||
* Separating the HostModel implementation from SDK repo repo aligns with code ownership, and facilitates maintenance.
|
||||
|
||||
The build targets/tasks that use the HostModel library are in the SDK repo because:
|
||||
|
||||
* This facilitates the MSBuild tasks to be multi-targeted.
|
||||
* It helps generate localized error messages, since SDK repo has the localization infrastructure.
|
||||
491
src/Microsoft.NET.HostModel/ResourceUpdater.cs
Normal file
491
src/Microsoft.NET.HostModel/ResourceUpdater.cs
Normal file
@@ -0,0 +1,491 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.NET.HostModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods for modifying the embedded native resources
|
||||
/// in a PE image. It currently only works on Windows, because it
|
||||
/// requires various kernel32 APIs.
|
||||
/// </summary>
|
||||
public class ResourceUpdater : IDisposable
|
||||
{
|
||||
private sealed class Kernel32
|
||||
{
|
||||
//
|
||||
// Native methods for updating resources
|
||||
//
|
||||
|
||||
[DllImport(nameof(Kernel32), CharSet = CharSet.Unicode, SetLastError=true)]
|
||||
public static extern SafeUpdateHandle BeginUpdateResource(string pFileName,
|
||||
[MarshalAs(UnmanagedType.Bool)]bool bDeleteExistingResources);
|
||||
|
||||
// Update a resource with data from an IntPtr
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool UpdateResource(SafeUpdateHandle hUpdate,
|
||||
IntPtr lpType,
|
||||
IntPtr lpName,
|
||||
ushort wLanguage,
|
||||
IntPtr lpData,
|
||||
uint cbData);
|
||||
|
||||
// Update a resource with data from a managed byte[]
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool UpdateResource(SafeUpdateHandle hUpdate,
|
||||
IntPtr lpType,
|
||||
IntPtr lpName,
|
||||
ushort wLanguage,
|
||||
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex=5)] byte[] lpData,
|
||||
uint cbData);
|
||||
|
||||
// Update a resource with data from a managed byte[]
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool UpdateResource(SafeUpdateHandle hUpdate,
|
||||
string lpType,
|
||||
IntPtr lpName,
|
||||
ushort wLanguage,
|
||||
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex=5)] byte[] lpData,
|
||||
uint cbData);
|
||||
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool EndUpdateResource(SafeUpdateHandle hUpdate,
|
||||
bool fDiscard);
|
||||
|
||||
// The IntPtr version of this dllimport is used in the
|
||||
// SafeHandle implementation
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool EndUpdateResource(IntPtr hUpdate,
|
||||
bool fDiscard);
|
||||
|
||||
public const ushort LangID_LangNeutral_SublangNeutral = 0;
|
||||
|
||||
//
|
||||
// Native methods used to read resources from a PE file
|
||||
//
|
||||
|
||||
// Loading and freeing PE files
|
||||
|
||||
public enum LoadLibraryFlags : uint
|
||||
{
|
||||
LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE = 0x00000040,
|
||||
LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x00000020
|
||||
}
|
||||
|
||||
[DllImport(nameof(Kernel32), CharSet = CharSet.Unicode, SetLastError=true)]
|
||||
public static extern IntPtr LoadLibraryEx(string lpFileName,
|
||||
IntPtr hReservedNull,
|
||||
LoadLibraryFlags dwFlags);
|
||||
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool FreeLibrary(IntPtr hModule);
|
||||
|
||||
// Enumerating resources
|
||||
|
||||
public delegate bool EnumResTypeProc(IntPtr hModule,
|
||||
IntPtr lpType,
|
||||
IntPtr lParam);
|
||||
|
||||
public delegate bool EnumResNameProc(IntPtr hModule,
|
||||
IntPtr lpType,
|
||||
IntPtr lpName,
|
||||
IntPtr lParam);
|
||||
|
||||
public delegate bool EnumResLangProc(IntPtr hModule,
|
||||
IntPtr lpType,
|
||||
IntPtr lpName,
|
||||
ushort wLang,
|
||||
IntPtr lParam);
|
||||
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool EnumResourceTypes(IntPtr hModule,
|
||||
EnumResTypeProc lpEnumFunc,
|
||||
IntPtr lParam);
|
||||
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool EnumResourceNames(IntPtr hModule,
|
||||
IntPtr lpType,
|
||||
EnumResNameProc lpEnumFunc,
|
||||
IntPtr lParam);
|
||||
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool EnumResourceLanguages(IntPtr hModule,
|
||||
IntPtr lpType,
|
||||
IntPtr lpName,
|
||||
EnumResLangProc lpEnumFunc,
|
||||
IntPtr lParam);
|
||||
|
||||
public const int UserStoppedResourceEnumerationHRESULT = unchecked((int)0x80073B02);
|
||||
public const int ResourceDataNotFoundHRESULT = unchecked((int)0x80070714);
|
||||
|
||||
// Querying and loading resources
|
||||
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
public static extern IntPtr FindResourceEx(IntPtr hModule,
|
||||
IntPtr lpType,
|
||||
IntPtr lpName,
|
||||
ushort wLanguage);
|
||||
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
public static extern IntPtr LoadResource(IntPtr hModule,
|
||||
IntPtr hResInfo);
|
||||
|
||||
[DllImport(nameof(Kernel32))] // does not call SetLastError
|
||||
public static extern IntPtr LockResource(IntPtr hResData);
|
||||
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
public static extern uint SizeofResource(IntPtr hModule,
|
||||
IntPtr hResInfo);
|
||||
|
||||
public const int ERROR_CALL_NOT_IMPLEMENTED = 0x78;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the update handle returned by BeginUpdateResource.
|
||||
/// Normally, native resources for the update handle are
|
||||
/// released by a call to ResourceUpdater.Update(). In case
|
||||
/// this doesn't happen, the SafeUpdateHandle will release the
|
||||
/// native resources for the update handle without updating
|
||||
/// the target file.
|
||||
/// </summary>
|
||||
private sealed class SafeUpdateHandle : SafeHandle
|
||||
{
|
||||
public SafeUpdateHandle() : base(IntPtr.Zero, true)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool IsInvalid => handle == IntPtr.Zero;
|
||||
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
// discard pending updates without writing them
|
||||
return Kernel32.EndUpdateResource(handle, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the native handle for the resource update.
|
||||
/// </summary>
|
||||
private readonly SafeUpdateHandle hUpdate;
|
||||
|
||||
///<summary>
|
||||
/// Determines if the ResourceUpdater is supported by the current operating system.
|
||||
/// Some versions of Windows, such as Nano Server, do not support the needed APIs.
|
||||
/// </summary>
|
||||
public static bool IsSupportedOS()
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// On Nano Server 1709+, `BeginUpdateResource` is exported but returns a null handle with a zero error
|
||||
// Try to call `BeginUpdateResource` with an invalid parameter; the error should be non-zero if supported
|
||||
// On Nano Server 20213, `BeginUpdateResource` fails with ERROR_CALL_NOT_IMPLEMENTED
|
||||
using (var handle = Kernel32.BeginUpdateResource("", false))
|
||||
{
|
||||
int lastWin32Error = Marshal.GetLastWin32Error();
|
||||
|
||||
if (handle.IsInvalid && (lastWin32Error == 0 || lastWin32Error == Kernel32.ERROR_CALL_NOT_IMPLEMENTED))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (EntryPointNotFoundException)
|
||||
{
|
||||
// BeginUpdateResource isn't exported from Kernel32
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a resource updater for the given PE file. This will
|
||||
/// acquire a native resource update handle for the file,
|
||||
/// preparing it for updates. Resources can be added to this
|
||||
/// updater, which will queue them for update. The target PE
|
||||
/// file will not be modified until Update() is called, after
|
||||
/// which the ResourceUpdater can not be used for further
|
||||
/// updates.
|
||||
/// </summary>
|
||||
public ResourceUpdater(string peFile)
|
||||
{
|
||||
hUpdate = Kernel32.BeginUpdateResource(peFile, false);
|
||||
if (hUpdate.IsInvalid)
|
||||
{
|
||||
ThrowExceptionForLastWin32Error();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add all resources from a source PE file. It is assumed
|
||||
/// that the input is a valid PE file. If it is not, an
|
||||
/// exception will be thrown. This will not modify the target
|
||||
/// until Update() is called.
|
||||
/// Throws an InvalidOperationException if Update() was already called.
|
||||
/// </summary>
|
||||
public ResourceUpdater AddResourcesFromPEImage(string peFile)
|
||||
{
|
||||
if (hUpdate.IsInvalid)
|
||||
{
|
||||
ThrowExceptionForInvalidUpdate();
|
||||
}
|
||||
|
||||
// Using both flags lets the OS loader decide how to load
|
||||
// it most efficiently. Either mode will prevent other
|
||||
// processes from modifying the module while it is loaded.
|
||||
IntPtr hModule = Kernel32.LoadLibraryEx(peFile, IntPtr.Zero,
|
||||
Kernel32.LoadLibraryFlags.LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE |
|
||||
Kernel32.LoadLibraryFlags.LOAD_LIBRARY_AS_IMAGE_RESOURCE);
|
||||
if (hModule == IntPtr.Zero)
|
||||
{
|
||||
ThrowExceptionForLastWin32Error();
|
||||
}
|
||||
|
||||
var enumTypesCallback = new Kernel32.EnumResTypeProc(EnumAndUpdateTypesCallback);
|
||||
var errorInfo = new EnumResourcesErrorInfo();
|
||||
GCHandle errorInfoHandle = GCHandle.Alloc(errorInfo);
|
||||
var errorInfoPtr = GCHandle.ToIntPtr(errorInfoHandle);
|
||||
|
||||
try
|
||||
{
|
||||
if (!Kernel32.EnumResourceTypes(hModule, enumTypesCallback, errorInfoPtr))
|
||||
{
|
||||
if (Marshal.GetHRForLastWin32Error() != Kernel32.ResourceDataNotFoundHRESULT)
|
||||
{
|
||||
CaptureEnumResourcesErrorInfo(errorInfoPtr);
|
||||
errorInfo.ThrowException();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
errorInfoHandle.Free();
|
||||
|
||||
if (!Kernel32.FreeLibrary(hModule))
|
||||
{
|
||||
ThrowExceptionForLastWin32Error();
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
internal static bool IsIntResource(IntPtr lpType)
|
||||
{
|
||||
return ((uint)lpType >> 16) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a language-neutral integer resource from a byte[] with
|
||||
/// a particular type and name. This will not modify the
|
||||
/// target until Update() is called.
|
||||
/// Throws an InvalidOperationException if Update() was already called.
|
||||
/// </summary>
|
||||
public ResourceUpdater AddResource(byte[] data, IntPtr lpType, IntPtr lpName)
|
||||
{
|
||||
if (hUpdate.IsInvalid)
|
||||
{
|
||||
ThrowExceptionForInvalidUpdate();
|
||||
}
|
||||
|
||||
if (!IsIntResource(lpType) || !IsIntResource(lpName))
|
||||
{
|
||||
throw new ArgumentException("AddResource can only be used with integer resource types");
|
||||
}
|
||||
|
||||
if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, Kernel32.LangID_LangNeutral_SublangNeutral, data, (uint)data.Length))
|
||||
{
|
||||
ThrowExceptionForLastWin32Error();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a language-neutral integer resource from a byte[] with
|
||||
/// a particular type and name. This will not modify the
|
||||
/// target until Update() is called.
|
||||
/// Throws an InvalidOperationException if Update() was already called.
|
||||
/// </summary>
|
||||
public ResourceUpdater AddResource(byte[] data, string lpType, IntPtr lpName)
|
||||
{
|
||||
if (hUpdate.IsInvalid)
|
||||
{
|
||||
ThrowExceptionForInvalidUpdate();
|
||||
}
|
||||
|
||||
if (!IsIntResource(lpName))
|
||||
{
|
||||
throw new ArgumentException("AddResource can only be used with integer resource names");
|
||||
}
|
||||
|
||||
if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, Kernel32.LangID_LangNeutral_SublangNeutral, data, (uint)data.Length))
|
||||
{
|
||||
ThrowExceptionForLastWin32Error();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the pending resource updates to the target PE
|
||||
/// file. After this, the ResourceUpdater no longer maintains
|
||||
/// an update handle, and can not be used for further updates.
|
||||
/// Throws an InvalidOperationException if Update() was already called.
|
||||
/// </summary>
|
||||
public void Update()
|
||||
{
|
||||
if (hUpdate.IsInvalid)
|
||||
{
|
||||
ThrowExceptionForInvalidUpdate();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!Kernel32.EndUpdateResource(hUpdate, false))
|
||||
{
|
||||
ThrowExceptionForLastWin32Error();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
hUpdate.SetHandleAsInvalid();
|
||||
}
|
||||
}
|
||||
|
||||
private bool EnumAndUpdateTypesCallback(IntPtr hModule, IntPtr lpType, IntPtr lParam)
|
||||
{
|
||||
var enumNamesCallback = new Kernel32.EnumResNameProc(EnumAndUpdateNamesCallback);
|
||||
if (!Kernel32.EnumResourceNames(hModule, lpType, enumNamesCallback, lParam))
|
||||
{
|
||||
CaptureEnumResourcesErrorInfo(lParam);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool EnumAndUpdateNamesCallback(IntPtr hModule, IntPtr lpType, IntPtr lpName, IntPtr lParam)
|
||||
{
|
||||
var enumLanguagesCallback = new Kernel32.EnumResLangProc(EnumAndUpdateLanguagesCallback);
|
||||
if (!Kernel32.EnumResourceLanguages(hModule, lpType, lpName, enumLanguagesCallback, lParam))
|
||||
{
|
||||
CaptureEnumResourcesErrorInfo(lParam);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool EnumAndUpdateLanguagesCallback(IntPtr hModule, IntPtr lpType, IntPtr lpName, ushort wLang, IntPtr lParam)
|
||||
{
|
||||
IntPtr hResource = Kernel32.FindResourceEx(hModule, lpType, lpName, wLang);
|
||||
if (hResource == IntPtr.Zero)
|
||||
{
|
||||
CaptureEnumResourcesErrorInfo(lParam);
|
||||
return false;
|
||||
}
|
||||
|
||||
// hResourceLoaded is just a handle to the resource, which
|
||||
// can be used to get the resource data
|
||||
IntPtr hResourceLoaded = Kernel32.LoadResource(hModule, hResource);
|
||||
if (hResourceLoaded == IntPtr.Zero)
|
||||
{
|
||||
CaptureEnumResourcesErrorInfo(lParam);
|
||||
return false;
|
||||
}
|
||||
|
||||
// This doesn't actually lock memory. It just retrieves a
|
||||
// pointer to the resource data. The pointer is valid
|
||||
// until the module is unloaded.
|
||||
IntPtr lpResourceData = Kernel32.LockResource(hResourceLoaded);
|
||||
if (lpResourceData == IntPtr.Zero)
|
||||
{
|
||||
((EnumResourcesErrorInfo)GCHandle.FromIntPtr(lParam).Target).failedToLockResource = true;
|
||||
}
|
||||
|
||||
if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, wLang, lpResourceData, Kernel32.SizeofResource(hModule, hResource)))
|
||||
{
|
||||
CaptureEnumResourcesErrorInfo(lParam);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private class EnumResourcesErrorInfo
|
||||
{
|
||||
public int hResult;
|
||||
public bool failedToLockResource;
|
||||
|
||||
public void ThrowException()
|
||||
{
|
||||
if (failedToLockResource)
|
||||
{
|
||||
Debug.Assert(hResult == 0);
|
||||
throw new ResourceNotAvailableException("Failed to lock resource");
|
||||
}
|
||||
|
||||
Debug.Assert(hResult != 0);
|
||||
throw new HResultException(hResult);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CaptureEnumResourcesErrorInfo(IntPtr errorInfoPtr)
|
||||
{
|
||||
int hResult = Marshal.GetHRForLastWin32Error();
|
||||
if (hResult != Kernel32.UserStoppedResourceEnumerationHRESULT)
|
||||
{
|
||||
GCHandle errorInfoHandle = GCHandle.FromIntPtr(errorInfoPtr);
|
||||
var errorInfo = (EnumResourcesErrorInfo)errorInfoHandle.Target;
|
||||
errorInfo.hResult = hResult;
|
||||
}
|
||||
}
|
||||
|
||||
private class ResourceNotAvailableException : Exception
|
||||
{
|
||||
public ResourceNotAvailableException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static void ThrowExceptionForLastWin32Error()
|
||||
{
|
||||
throw new HResultException(Marshal.GetHRForLastWin32Error());
|
||||
}
|
||||
|
||||
private static void ThrowExceptionForInvalidUpdate()
|
||||
{
|
||||
throw new InvalidOperationException("Update handle is invalid. This instance may not be used for further updates");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
hUpdate.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
92
src/SquirrelCli/HostModel/AppHost/AppHostExceptions.cs
Normal file
92
src/SquirrelCli/HostModel/AppHost/AppHostExceptions.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.NET.HostModel.AppHost
|
||||
{
|
||||
/// <summary>
|
||||
/// An instance of this exception is thrown when an AppHost binary update
|
||||
/// fails due to known user errors.
|
||||
/// </summary>
|
||||
public class AppHostUpdateException : Exception
|
||||
{
|
||||
internal AppHostUpdateException(string message = null)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The application host executable cannot be customized because adding resources requires
|
||||
/// that the build be performed on Windows (excluding Nano Server).
|
||||
/// </summary>
|
||||
public sealed class AppHostCustomizationUnsupportedOSException : AppHostUpdateException
|
||||
{
|
||||
internal AppHostCustomizationUnsupportedOSException()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The MachO application host executable cannot be customized because
|
||||
/// it was not in the expected format
|
||||
/// </summary>
|
||||
public sealed class AppHostMachOFormatException : AppHostUpdateException
|
||||
{
|
||||
public readonly MachOFormatError Error;
|
||||
|
||||
internal AppHostMachOFormatException(MachOFormatError error)
|
||||
{
|
||||
Error = error;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unable to use the input file as application host executable because it's not a
|
||||
/// Windows executable for the CUI (Console) subsystem.
|
||||
/// </summary>
|
||||
public sealed class AppHostNotCUIException : AppHostUpdateException
|
||||
{
|
||||
internal AppHostNotCUIException()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unable to use the input file as an application host executable
|
||||
/// because it's not a Windows PE file
|
||||
/// </summary>
|
||||
public sealed class AppHostNotPEFileException : AppHostUpdateException
|
||||
{
|
||||
internal AppHostNotPEFileException()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unable to sign the apphost binary.
|
||||
/// </summary>
|
||||
public sealed class AppHostSigningException : AppHostUpdateException
|
||||
{
|
||||
public readonly int ExitCode;
|
||||
|
||||
internal AppHostSigningException(int exitCode, string signingErrorMessage)
|
||||
: base(signingErrorMessage)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given app file name is longer than 1024 bytes
|
||||
/// </summary>
|
||||
public sealed class AppNameTooLongException : AppHostUpdateException
|
||||
{
|
||||
public string LongName { get; }
|
||||
|
||||
internal AppNameTooLongException(string name)
|
||||
{
|
||||
LongName = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
202
src/SquirrelCli/HostModel/AppHost/BinaryUtils.cs
Normal file
202
src/SquirrelCli/HostModel/AppHost/BinaryUtils.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
|
||||
namespace Microsoft.NET.HostModel.AppHost
|
||||
{
|
||||
public static class BinaryUtils
|
||||
{
|
||||
internal static unsafe void SearchAndReplace(
|
||||
MemoryMappedViewAccessor accessor,
|
||||
byte[] searchPattern,
|
||||
byte[] patternToReplace,
|
||||
bool pad0s = true)
|
||||
{
|
||||
byte* pointer = null;
|
||||
|
||||
try
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
|
||||
byte* bytes = pointer + accessor.PointerOffset;
|
||||
|
||||
int position = KMPSearch(searchPattern, bytes, accessor.Capacity);
|
||||
if (position < 0)
|
||||
{
|
||||
throw new PlaceHolderNotFoundInAppHostException(searchPattern);
|
||||
}
|
||||
|
||||
accessor.WriteArray(
|
||||
position: position,
|
||||
array: patternToReplace,
|
||||
offset: 0,
|
||||
count: patternToReplace.Length);
|
||||
|
||||
if (pad0s)
|
||||
{
|
||||
Pad0(searchPattern, patternToReplace, bytes, position);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pointer != null)
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void Pad0(byte[] searchPattern, byte[] patternToReplace, byte* bytes, int offset)
|
||||
{
|
||||
if (patternToReplace.Length < searchPattern.Length)
|
||||
{
|
||||
for (int i = patternToReplace.Length; i < searchPattern.Length; i++)
|
||||
{
|
||||
bytes[i + offset] = 0x0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe void SearchAndReplace(
|
||||
string filePath,
|
||||
byte[] searchPattern,
|
||||
byte[] patternToReplace,
|
||||
bool pad0s = true)
|
||||
{
|
||||
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
|
||||
{
|
||||
using (var accessor = mappedFile.CreateViewAccessor())
|
||||
{
|
||||
SearchAndReplace(accessor, searchPattern, patternToReplace, pad0s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static unsafe int SearchInFile(MemoryMappedViewAccessor accessor, byte[] searchPattern)
|
||||
{
|
||||
var safeBuffer = accessor.SafeMemoryMappedViewHandle;
|
||||
return KMPSearch(searchPattern, (byte*)safeBuffer.DangerousGetHandle(), (int)safeBuffer.ByteLength);
|
||||
}
|
||||
|
||||
public static unsafe int SearchInFile(string filePath, byte[] searchPattern)
|
||||
{
|
||||
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
|
||||
{
|
||||
using (var accessor = mappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read))
|
||||
{
|
||||
return SearchInFile(accessor, searchPattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm
|
||||
private static int[] ComputeKMPFailureFunction(byte[] pattern)
|
||||
{
|
||||
int[] table = new int[pattern.Length];
|
||||
if (pattern.Length >= 1)
|
||||
{
|
||||
table[0] = -1;
|
||||
}
|
||||
if (pattern.Length >= 2)
|
||||
{
|
||||
table[1] = 0;
|
||||
}
|
||||
|
||||
int pos = 2;
|
||||
int cnd = 0;
|
||||
while (pos < pattern.Length)
|
||||
{
|
||||
if (pattern[pos - 1] == pattern[cnd])
|
||||
{
|
||||
table[pos] = cnd + 1;
|
||||
cnd++;
|
||||
pos++;
|
||||
}
|
||||
else if (cnd > 0)
|
||||
{
|
||||
cnd = table[cnd];
|
||||
}
|
||||
else
|
||||
{
|
||||
table[pos] = 0;
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
// See: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm
|
||||
private static unsafe int KMPSearch(byte[] pattern, byte* bytes, long bytesLength)
|
||||
{
|
||||
int m = 0;
|
||||
int i = 0;
|
||||
int[] table = ComputeKMPFailureFunction(pattern);
|
||||
|
||||
while (m + i < bytesLength)
|
||||
{
|
||||
if (pattern[i] == bytes[m + i])
|
||||
{
|
||||
if (i == pattern.Length - 1)
|
||||
{
|
||||
return m;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (table[i] > -1)
|
||||
{
|
||||
m = m + i - table[i];
|
||||
i = table[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
m++;
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static void CopyFile(string sourcePath, string destinationPath)
|
||||
{
|
||||
var destinationDirectory = new FileInfo(destinationPath).Directory.FullName;
|
||||
if (!Directory.Exists(destinationDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(destinationDirectory);
|
||||
}
|
||||
|
||||
// Copy file to destination path so it inherits the same attributes/permissions.
|
||||
File.Copy(sourcePath, destinationPath, overwrite: true);
|
||||
}
|
||||
|
||||
internal static void WriteToStream(MemoryMappedViewAccessor sourceViewAccessor, FileStream fileStream, long length)
|
||||
{
|
||||
int pos = 0;
|
||||
int bufSize = 16384; //16K
|
||||
|
||||
byte[] buf = new byte[bufSize];
|
||||
length = Math.Min(length, sourceViewAccessor.Capacity);
|
||||
do
|
||||
{
|
||||
int bytesRequested = Math.Min((int)length - pos, bufSize);
|
||||
if (bytesRequested <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
int bytesRead = sourceViewAccessor.ReadArray(pos, buf, 0, bytesRequested);
|
||||
if (bytesRead > 0)
|
||||
{
|
||||
fileStream.Write(buf, 0, bytesRead);
|
||||
pos += bytesRead;
|
||||
}
|
||||
}
|
||||
while (true);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/SquirrelCli/HostModel/AppHost/ElfUtils.cs
Normal file
51
src/SquirrelCli/HostModel/AppHost/ElfUtils.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.NET.HostModel.AppHost
|
||||
{
|
||||
internal static class ElfUtils
|
||||
{
|
||||
// The Linux Headers are copied from elf.h
|
||||
|
||||
#pragma warning disable 0649
|
||||
private struct ElfHeader
|
||||
{
|
||||
private byte EI_MAG0;
|
||||
private byte EI_MAG1;
|
||||
private byte EI_MAG2;
|
||||
private byte EI_MAG3;
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return EI_MAG0 == 0x7f &&
|
||||
EI_MAG1 == 0x45 &&
|
||||
EI_MAG2 == 0x4C &&
|
||||
EI_MAG3 == 0x46;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
#pragma warning restore 0649
|
||||
|
||||
public static bool IsElfImage(string filePath)
|
||||
{
|
||||
using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
|
||||
{
|
||||
if (reader.BaseStream.Length < 16) // EI_NIDENT = 16
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] eIdent = reader.ReadBytes(4);
|
||||
|
||||
// Check that the first four bytes are 0x7f, 'E', 'L', 'F'
|
||||
return eIdent[0] == 0x7f &&
|
||||
eIdent[1] == 0x45 &&
|
||||
eIdent[2] == 0x4C &&
|
||||
eIdent[3] == 0x46;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/SquirrelCli/HostModel/AppHost/HResultException.cs
Normal file
22
src/SquirrelCli/HostModel/AppHost/HResultException.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.NET.HostModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an exception thrown because of a Win32 error
|
||||
/// </summary>
|
||||
public class HResultException : Exception
|
||||
{
|
||||
public readonly int Win32HResult;
|
||||
public HResultException(int hResult) : base(hResult.ToString("X4"))
|
||||
{
|
||||
Win32HResult = hResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
267
src/SquirrelCli/HostModel/AppHost/HostWriter.cs
Normal file
267
src/SquirrelCli/HostModel/AppHost/HostWriter.cs
Normal file
@@ -0,0 +1,267 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.NET.HostModel.AppHost
|
||||
{
|
||||
/// <summary>
|
||||
/// Embeds the App Name into the AppHost.exe
|
||||
/// If an apphost is a single-file bundle, updates the location of the bundle headers.
|
||||
/// </summary>
|
||||
public static class HostWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// hash value embedded in default apphost executable in a place where the path to the app binary should be stored.
|
||||
/// </summary>
|
||||
private const string AppBinaryPathPlaceholder = "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2";
|
||||
private static readonly byte[] AppBinaryPathPlaceholderSearchValue = Encoding.UTF8.GetBytes(AppBinaryPathPlaceholder);
|
||||
|
||||
/// <summary>
|
||||
/// Create an AppHost with embedded configuration of app binary location
|
||||
/// </summary>
|
||||
/// <param name="appHostSourceFilePath">The path of Apphost template, which has the place holder</param>
|
||||
/// <param name="appHostDestinationFilePath">The destination path for desired location to place, including the file name</param>
|
||||
/// <param name="appBinaryFilePath">Full path to app binary or relative path to the result apphost file</param>
|
||||
/// <param name="windowsGraphicalUserInterface">Specify whether to set the subsystem to GUI. Only valid for PE apphosts.</param>
|
||||
/// <param name="assemblyToCopyResorcesFrom">Path to the intermediate assembly, used for copying resources to PE apphosts.</param>
|
||||
/// <param name="enableMacOSCodeSign">Sign the app binary using codesign with an anonymous certificate.</param>
|
||||
public static void CreateAppHost(
|
||||
string appHostSourceFilePath,
|
||||
string appHostDestinationFilePath,
|
||||
string appBinaryFilePath,
|
||||
bool windowsGraphicalUserInterface = false,
|
||||
string assemblyToCopyResorcesFrom = null,
|
||||
bool enableMacOSCodeSign = false)
|
||||
{
|
||||
var bytesToWrite = Encoding.UTF8.GetBytes(appBinaryFilePath);
|
||||
if (bytesToWrite.Length > 1024)
|
||||
{
|
||||
throw new AppNameTooLongException(appBinaryFilePath);
|
||||
}
|
||||
|
||||
bool appHostIsPEImage = false;
|
||||
|
||||
void RewriteAppHost(MemoryMappedViewAccessor accessor)
|
||||
{
|
||||
// Re-write the destination apphost with the proper contents.
|
||||
BinaryUtils.SearchAndReplace(accessor, AppBinaryPathPlaceholderSearchValue, bytesToWrite);
|
||||
|
||||
appHostIsPEImage = PEUtils.IsPEImage(accessor);
|
||||
|
||||
if (windowsGraphicalUserInterface)
|
||||
{
|
||||
if (!appHostIsPEImage)
|
||||
{
|
||||
throw new AppHostNotPEFileException();
|
||||
}
|
||||
|
||||
PEUtils.SetWindowsGraphicalUserInterfaceBit(accessor);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateResources()
|
||||
{
|
||||
if (assemblyToCopyResorcesFrom != null && appHostIsPEImage)
|
||||
{
|
||||
if (ResourceUpdater.IsSupportedOS())
|
||||
{
|
||||
// Copy resources from managed dll to the apphost
|
||||
new ResourceUpdater(appHostDestinationFilePath)
|
||||
.AddResourcesFromPEImage(assemblyToCopyResorcesFrom)
|
||||
.Update();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new AppHostCustomizationUnsupportedOSException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RetryUtil.RetryOnIOError(() =>
|
||||
{
|
||||
FileStream appHostSourceStream = null;
|
||||
MemoryMappedFile memoryMappedFile = null;
|
||||
MemoryMappedViewAccessor memoryMappedViewAccessor = null;
|
||||
try
|
||||
{
|
||||
// Open the source host file.
|
||||
appHostSourceStream = new FileStream(appHostSourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostSourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true);
|
||||
memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.CopyOnWrite);
|
||||
|
||||
// Get the size of the source app host to ensure that we don't write extra data to the destination.
|
||||
// On Windows, the size of the view accessor is rounded up to the next page boundary.
|
||||
long sourceAppHostLength = appHostSourceStream.Length;
|
||||
|
||||
// Transform the host file in-memory.
|
||||
RewriteAppHost(memoryMappedViewAccessor);
|
||||
|
||||
// Save the transformed host.
|
||||
using (FileStream fileStream = new FileStream(appHostDestinationFilePath, FileMode.Create))
|
||||
{
|
||||
BinaryUtils.WriteToStream(memoryMappedViewAccessor, fileStream, sourceAppHostLength);
|
||||
|
||||
// Remove the signature from MachO hosts.
|
||||
if (!appHostIsPEImage)
|
||||
{
|
||||
MachOUtils.RemoveSignature(fileStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
memoryMappedViewAccessor?.Dispose();
|
||||
memoryMappedFile?.Dispose();
|
||||
appHostSourceStream?.Dispose();
|
||||
}
|
||||
});
|
||||
|
||||
RetryUtil.RetryOnWin32Error(UpdateResources);
|
||||
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
var filePermissionOctal = Convert.ToInt32("755", 8); // -rwxr-xr-x
|
||||
const int EINTR = 4;
|
||||
int chmodReturnCode = 0;
|
||||
|
||||
do
|
||||
{
|
||||
chmodReturnCode = chmod(appHostDestinationFilePath, filePermissionOctal);
|
||||
}
|
||||
while (chmodReturnCode == -1 && Marshal.GetLastWin32Error() == EINTR);
|
||||
|
||||
if (chmodReturnCode == -1)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not set file permission {filePermissionOctal} for {appHostDestinationFilePath}.");
|
||||
}
|
||||
|
||||
if (enableMacOSCodeSign && RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
CodeSign(appHostDestinationFilePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Delete the destination file so we don't leave an unmodified apphost
|
||||
try
|
||||
{
|
||||
File.Delete(appHostDestinationFilePath);
|
||||
}
|
||||
catch (Exception failedToDeleteEx)
|
||||
{
|
||||
throw new AggregateException(ex, failedToDeleteEx);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the current AppHost as a single-file bundle.
|
||||
/// </summary>
|
||||
/// <param name="appHostPath">The path of Apphost template, which has the place holder</param>
|
||||
/// <param name="bundleHeaderOffset">The offset to the location of bundle header</param>
|
||||
public static void SetAsBundle(
|
||||
string appHostPath,
|
||||
long bundleHeaderOffset)
|
||||
{
|
||||
byte[] bundleHeaderPlaceholder = {
|
||||
// 8 bytes represent the bundle header-offset
|
||||
// Zero for non-bundle apphosts (default).
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// 32 bytes represent the bundle signature: SHA-256 for ".net core bundle"
|
||||
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
|
||||
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
|
||||
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
|
||||
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
|
||||
};
|
||||
|
||||
// Re-write the destination apphost with the proper contents.
|
||||
RetryUtil.RetryOnIOError(() =>
|
||||
BinaryUtils.SearchAndReplace(appHostPath,
|
||||
bundleHeaderPlaceholder,
|
||||
BitConverter.GetBytes(bundleHeaderOffset),
|
||||
pad0s: false));
|
||||
|
||||
RetryUtil.RetryOnIOError(() =>
|
||||
MachOUtils.AdjustHeadersForBundle(appHostPath));
|
||||
|
||||
// Memory-mapped write does not updating last write time
|
||||
RetryUtil.RetryOnIOError(() =>
|
||||
File.SetLastWriteTimeUtc(appHostPath, DateTime.UtcNow));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the an AppHost is a single-file bundle
|
||||
/// </summary>
|
||||
/// <param name="appHostFilePath">The path of Apphost to check</param>
|
||||
/// <param name="bundleHeaderOffset">An out parameter containing the offset of the bundle header (if any)</param>
|
||||
/// <returns>True if the AppHost is a single-file bundle, false otherwise</returns>
|
||||
public static bool IsBundle(string appHostFilePath, out long bundleHeaderOffset)
|
||||
{
|
||||
byte[] bundleSignature = {
|
||||
// 32 bytes represent the bundle signature: SHA-256 for ".net core bundle"
|
||||
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
|
||||
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
|
||||
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
|
||||
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
|
||||
};
|
||||
|
||||
long headerOffset = 0;
|
||||
void FindBundleHeader()
|
||||
{
|
||||
using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostFilePath))
|
||||
{
|
||||
using (MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor())
|
||||
{
|
||||
int position = BinaryUtils.SearchInFile(accessor, bundleSignature);
|
||||
if (position == -1)
|
||||
{
|
||||
throw new PlaceHolderNotFoundInAppHostException(bundleSignature);
|
||||
}
|
||||
|
||||
headerOffset = accessor.ReadInt64(position - sizeof(long));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RetryUtil.RetryOnIOError(FindBundleHeader);
|
||||
bundleHeaderOffset = headerOffset;
|
||||
|
||||
return headerOffset != 0;
|
||||
}
|
||||
|
||||
private static void CodeSign(string appHostPath)
|
||||
{
|
||||
Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.OSX));
|
||||
const string codesign = @"/usr/bin/codesign";
|
||||
if (!File.Exists(codesign))
|
||||
return;
|
||||
|
||||
var psi = new ProcessStartInfo()
|
||||
{
|
||||
Arguments = $"-s - \"{appHostPath}\"",
|
||||
FileName = codesign,
|
||||
RedirectStandardError = true,
|
||||
};
|
||||
|
||||
using (var p = Process.Start(psi))
|
||||
{
|
||||
p.WaitForExit();
|
||||
if (p.ExitCode != 0)
|
||||
throw new AppHostSigningException(p.ExitCode, p.StandardError.ReadToEnd());
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
private static extern int chmod(string pathname, int mode);
|
||||
}
|
||||
}
|
||||
26
src/SquirrelCli/HostModel/AppHost/MachOFormatError.cs
Normal file
26
src/SquirrelCli/HostModel/AppHost/MachOFormatError.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.NET.HostModel.AppHost
|
||||
{
|
||||
/// <summary>
|
||||
/// Additional details about the failure with caused an AppHostMachOFormatException
|
||||
/// </summary>
|
||||
public enum MachOFormatError
|
||||
{
|
||||
Not64BitExe, // Apphost is expected to be a 64-bit MachO executable
|
||||
DuplicateLinkEdit, // Only one __LINKEDIT segment is expected in the apphost
|
||||
DuplicateSymtab, // Only one SYMTAB is expected in the apphost
|
||||
MissingLinkEdit, // CODE_SIGNATURE command must follow a Segment64 command named __LINKEDIT
|
||||
MissingSymtab, // CODE_SIGNATURE command must follow the SYMTAB command
|
||||
LinkEditNotLast, // __LINKEDIT must be the last segment in the binary layout
|
||||
SymtabNotInLinkEdit, // SYMTAB must within the __LINKEDIT segment!
|
||||
SignNotInLinkEdit, // Signature blob must be within the __LINKEDIT segment!
|
||||
SignCommandNotLast, // CODE_SIGNATURE command must be the last command
|
||||
SignBlobNotLast, // Signature blob must be at the very end of the file
|
||||
SignDoesntFollowSymtab, // Signature blob must immediately follow the Symtab
|
||||
MemoryMapAccessFault, // Error reading the memory-mapped apphost
|
||||
InvalidUTF8, // UTF8 decoding failed
|
||||
SignNotRemoved, // Signature not removed from the host (while processing a single-file bundle)
|
||||
}
|
||||
}
|
||||
446
src/SquirrelCli/HostModel/AppHost/MachOUtils.cs
Normal file
446
src/SquirrelCli/HostModel/AppHost/MachOUtils.cs
Normal file
@@ -0,0 +1,446 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.NET.HostModel.AppHost
|
||||
{
|
||||
internal static class MachOUtils
|
||||
{
|
||||
// The MachO Headers are copied from
|
||||
// https://opensource.apple.com/source/cctools/cctools-870/include/mach-o/loader.h
|
||||
//
|
||||
// The data fields and enumerations match the structure definitions in the above file,
|
||||
// and hence do not conform to C# CoreFx naming style.
|
||||
|
||||
private enum Magic : uint
|
||||
{
|
||||
MH_MAGIC = 0xfeedface,
|
||||
MH_CIGAM = 0xcefaedfe,
|
||||
MH_MAGIC_64 = 0xfeedfacf,
|
||||
MH_CIGAM_64 = 0xcffaedfe
|
||||
}
|
||||
|
||||
private enum FileType : uint
|
||||
{
|
||||
MH_EXECUTE = 0x2
|
||||
}
|
||||
|
||||
#pragma warning disable 0649
|
||||
private struct MachHeader
|
||||
{
|
||||
public Magic magic;
|
||||
public int cputype;
|
||||
public int cpusubtype;
|
||||
public FileType filetype;
|
||||
public uint ncmds;
|
||||
public uint sizeofcmds;
|
||||
public uint flags;
|
||||
public uint reserved;
|
||||
|
||||
public bool Is64BitExecutable()
|
||||
{
|
||||
return magic == Magic.MH_MAGIC_64 && filetype == FileType.MH_EXECUTE;
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
switch (magic)
|
||||
{
|
||||
case Magic.MH_CIGAM:
|
||||
case Magic.MH_CIGAM_64:
|
||||
case Magic.MH_MAGIC:
|
||||
case Magic.MH_MAGIC_64:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum Command : uint
|
||||
{
|
||||
LC_SYMTAB = 0x2,
|
||||
LC_SEGMENT_64 = 0x19,
|
||||
LC_CODE_SIGNATURE = 0x1d,
|
||||
}
|
||||
|
||||
private struct LoadCommand
|
||||
{
|
||||
public Command cmd;
|
||||
public uint cmdsize;
|
||||
}
|
||||
|
||||
// The linkedit_data_command contains the offsets and sizes of a blob
|
||||
// of data in the __LINKEDIT segment (including LC_CODE_SIGNATURE).
|
||||
private struct LinkEditDataCommand
|
||||
{
|
||||
public Command cmd;
|
||||
public uint cmdsize;
|
||||
public uint dataoff;
|
||||
public uint datasize;
|
||||
}
|
||||
|
||||
private struct SymtabCommand
|
||||
{
|
||||
public uint cmd;
|
||||
public uint cmdsize;
|
||||
public uint symoff;
|
||||
public uint nsyms;
|
||||
public uint stroff;
|
||||
public uint strsize;
|
||||
};
|
||||
|
||||
private unsafe struct SegmentCommand64
|
||||
{
|
||||
public Command cmd;
|
||||
public uint cmdsize;
|
||||
public fixed byte segname[16];
|
||||
public ulong vmaddr;
|
||||
public ulong vmsize;
|
||||
public ulong fileoff;
|
||||
public ulong filesize;
|
||||
public int maxprot;
|
||||
public int initprot;
|
||||
public uint nsects;
|
||||
public uint flags;
|
||||
|
||||
public string SegName
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (byte* p = segname)
|
||||
{
|
||||
int len = 0;
|
||||
while (*(p + len) != 0 && len++ < 16) ;
|
||||
|
||||
try
|
||||
{
|
||||
return Encoding.UTF8.GetString(p, len);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
throw new AppHostMachOFormatException(MachOFormatError.InvalidUTF8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning restore 0649
|
||||
|
||||
private static void Verify(bool condition, MachOFormatError error)
|
||||
{
|
||||
if (!condition)
|
||||
{
|
||||
throw new AppHostMachOFormatException(error);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsMachOImage(string filePath)
|
||||
{
|
||||
using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
|
||||
{
|
||||
if (reader.BaseStream.Length < 256) // Header size
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint magic = reader.ReadUInt32();
|
||||
return Enum.IsDefined(typeof(Magic), magic);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This Method is a utility to remove the code-signature (if any)
|
||||
/// from a MachO AppHost binary.
|
||||
///
|
||||
/// The tool assumes the following layout of the executable:
|
||||
///
|
||||
/// * MachoHeader (64-bit, executable, not swapped integers)
|
||||
/// * LoadCommands
|
||||
/// LC_SEGMENT_64 (__PAGEZERO)
|
||||
/// LC_SEGMENT_64 (__TEXT)
|
||||
/// LC_SEGMENT_64 (__DATA)
|
||||
/// LC_SEGMENT_64 (__LINKEDIT)
|
||||
/// ...
|
||||
/// LC_SYMTAB
|
||||
/// ...
|
||||
/// LC_CODE_SIGNATURE (last)
|
||||
///
|
||||
/// * ... Different Segments ...
|
||||
///
|
||||
/// * The __LINKEDIT Segment (last)
|
||||
/// * ... Different sections ...
|
||||
/// * SYMTAB
|
||||
/// * (Some alignment bytes)
|
||||
/// * The Code-signature
|
||||
///
|
||||
/// In order to remove the signature, the method:
|
||||
/// - Removes (zeros out) the LC_CODE_SIGNATURE command
|
||||
/// - Adjusts the size and count of the load commands in the header
|
||||
/// - Truncates the size of the __LINKEDIT segment to the end of SYMTAB
|
||||
/// - Truncates the apphost file to the end of the __LINKEDIT segment
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="stream">Stream containing the AppHost</param>
|
||||
/// <returns>
|
||||
/// True if
|
||||
/// - The input is a MachO binary, and
|
||||
/// - It is a signed binary, and
|
||||
/// - The signature was successfully removed
|
||||
/// False otherwise
|
||||
/// </returns>
|
||||
/// <exception cref="AppHostMachOFormatException">
|
||||
/// The input is a MachO file, but doesn't match the expect format of the AppHost.
|
||||
/// </exception>
|
||||
public static unsafe bool RemoveSignature(FileStream stream)
|
||||
{
|
||||
uint signatureSize = 0;
|
||||
using (var mappedFile = MemoryMappedFile.CreateFromFile(stream,
|
||||
mapName: null,
|
||||
capacity: 0,
|
||||
MemoryMappedFileAccess.ReadWrite,
|
||||
HandleInheritability.None,
|
||||
leaveOpen: true))
|
||||
{
|
||||
using (var accessor = mappedFile.CreateViewAccessor())
|
||||
{
|
||||
byte* file = null;
|
||||
try
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file);
|
||||
Verify(file != null, MachOFormatError.MemoryMapAccessFault);
|
||||
|
||||
MachHeader* header = (MachHeader*)file;
|
||||
|
||||
if (!header->IsValid())
|
||||
{
|
||||
// Not a MachO file.
|
||||
return false;
|
||||
}
|
||||
|
||||
Verify(header->Is64BitExecutable(), MachOFormatError.Not64BitExe);
|
||||
|
||||
file += sizeof(MachHeader);
|
||||
SegmentCommand64* linkEdit = null;
|
||||
SymtabCommand* symtab = null;
|
||||
LinkEditDataCommand* signature = null;
|
||||
|
||||
for (uint i = 0; i < header->ncmds; i++)
|
||||
{
|
||||
LoadCommand* command = (LoadCommand*)file;
|
||||
if (command->cmd == Command.LC_SEGMENT_64)
|
||||
{
|
||||
SegmentCommand64* segment = (SegmentCommand64*)file;
|
||||
if (segment->SegName.Equals("__LINKEDIT"))
|
||||
{
|
||||
Verify(linkEdit == null, MachOFormatError.DuplicateLinkEdit);
|
||||
linkEdit = segment;
|
||||
}
|
||||
}
|
||||
else if (command->cmd == Command.LC_SYMTAB)
|
||||
{
|
||||
Verify(symtab == null, MachOFormatError.DuplicateSymtab);
|
||||
symtab = (SymtabCommand*)command;
|
||||
}
|
||||
else if (command->cmd == Command.LC_CODE_SIGNATURE)
|
||||
{
|
||||
Verify(i == header->ncmds - 1, MachOFormatError.SignCommandNotLast);
|
||||
signature = (LinkEditDataCommand*)command;
|
||||
break;
|
||||
}
|
||||
|
||||
file += command->cmdsize;
|
||||
}
|
||||
|
||||
if (signature != null)
|
||||
{
|
||||
Verify(linkEdit != null, MachOFormatError.MissingLinkEdit);
|
||||
Verify(symtab != null, MachOFormatError.MissingSymtab);
|
||||
|
||||
var symtabEnd = symtab->stroff + symtab->strsize;
|
||||
var linkEditEnd = linkEdit->fileoff + linkEdit->filesize;
|
||||
var signatureEnd = signature->dataoff + signature->datasize;
|
||||
var fileEnd = (ulong)stream.Length;
|
||||
|
||||
Verify(linkEditEnd == fileEnd, MachOFormatError.LinkEditNotLast);
|
||||
Verify(signatureEnd == fileEnd, MachOFormatError.SignBlobNotLast);
|
||||
|
||||
Verify(symtab->symoff > linkEdit->fileoff, MachOFormatError.SymtabNotInLinkEdit);
|
||||
Verify(signature->dataoff > linkEdit->fileoff, MachOFormatError.SignNotInLinkEdit);
|
||||
|
||||
// The signature blob immediately follows the symtab blob,
|
||||
// except for a few bytes of padding.
|
||||
Verify(signature->dataoff >= symtabEnd && signature->dataoff - symtabEnd < 32, MachOFormatError.SignBlobNotLast);
|
||||
|
||||
// Remove the signature command
|
||||
header->ncmds--;
|
||||
header->sizeofcmds -= signature->cmdsize;
|
||||
Unsafe.InitBlock(signature, 0, signature->cmdsize);
|
||||
|
||||
// Remove the signature blob (note for truncation)
|
||||
signatureSize = (uint)(fileEnd - symtabEnd);
|
||||
|
||||
// Adjust the __LINKEDIT segment load command
|
||||
linkEdit->filesize -= signatureSize;
|
||||
|
||||
// codesign --remove-signature doesn't reset the vmsize.
|
||||
// Setting the vmsize here makes the output bin-equal with the original
|
||||
// unsigned apphost (and not bin-equal with a signed-unsigned-apphost).
|
||||
linkEdit->vmsize = linkEdit->filesize;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (file != null)
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (signatureSize != 0)
|
||||
{
|
||||
// The signature was removed, update the file length
|
||||
stream.SetLength(stream.Length - signatureSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This Method is a utility to adjust the apphost MachO-header
|
||||
/// to include the bytes added by the single-file bundler at the end of the file.
|
||||
///
|
||||
/// The tool assumes the following layout of the executable
|
||||
///
|
||||
/// * MachoHeader (64-bit, executable, not swapped integers)
|
||||
/// * LoadCommands
|
||||
/// LC_SEGMENT_64 (__PAGEZERO)
|
||||
/// LC_SEGMENT_64 (__TEXT)
|
||||
/// LC_SEGMENT_64 (__DATA)
|
||||
/// LC_SEGMENT_64 (__LINKEDIT)
|
||||
/// ...
|
||||
/// LC_SYMTAB
|
||||
///
|
||||
/// * ... Different Segments
|
||||
///
|
||||
/// * The __LINKEDIT Segment (last)
|
||||
/// * ... Different sections ...
|
||||
/// * SYMTAB (last)
|
||||
///
|
||||
/// The MAC codesign tool places several restrictions on the layout
|
||||
/// * The __LINKEDIT segment must be the last one
|
||||
/// * The __LINKEDIT segment must cover the end of the file
|
||||
/// * All bytes in the __LINKEDIT segment are used by other linkage commands
|
||||
/// (ex: symbol/string table, dynamic load information etc)
|
||||
///
|
||||
/// In order to circumvent these restrictions, we:
|
||||
/// * Extend the __LINKEDIT segment to include the bundle-data
|
||||
/// * Extend the string table to include all the bundle-data
|
||||
/// (that is, the bundle-data appear as strings to the loader/codesign tool).
|
||||
///
|
||||
/// This method has certain limitations:
|
||||
/// * The bytes for the bundler may be unnecessarily loaded at startup
|
||||
/// * Tools that process the string table may be confused (?)
|
||||
/// * The string table size is limited to 4GB. Bundles larger than that size
|
||||
/// cannot be accomodated by this utility.
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the AppHost</param>
|
||||
/// <returns>
|
||||
/// True if
|
||||
/// - The input is a MachO binary, and
|
||||
/// - The additional bytes were successfully accomodated within the MachO segments.
|
||||
/// False otherwise
|
||||
/// </returns>
|
||||
/// <exception cref="AppHostMachOFormatException">
|
||||
/// The input is a MachO file, but doesn't match the expect format of the AppHost.
|
||||
/// </exception>
|
||||
public static unsafe bool AdjustHeadersForBundle(string filePath)
|
||||
{
|
||||
ulong fileLength = (ulong)new FileInfo(filePath).Length;
|
||||
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
|
||||
{
|
||||
using (var accessor = mappedFile.CreateViewAccessor())
|
||||
{
|
||||
byte* file = null;
|
||||
try
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file);
|
||||
Verify(file != null, MachOFormatError.MemoryMapAccessFault);
|
||||
|
||||
MachHeader* header = (MachHeader*)file;
|
||||
|
||||
if (!header->IsValid())
|
||||
{
|
||||
// Not a MachO file.
|
||||
return false;
|
||||
}
|
||||
|
||||
Verify(header->Is64BitExecutable(), MachOFormatError.Not64BitExe);
|
||||
|
||||
file += sizeof(MachHeader);
|
||||
SegmentCommand64* linkEdit = null;
|
||||
SymtabCommand* symtab = null;
|
||||
LinkEditDataCommand* signature = null;
|
||||
|
||||
for (uint i = 0; i < header->ncmds; i++)
|
||||
{
|
||||
LoadCommand* command = (LoadCommand*)file;
|
||||
if (command->cmd == Command.LC_SEGMENT_64)
|
||||
{
|
||||
SegmentCommand64* segment = (SegmentCommand64*)file;
|
||||
if (segment->SegName.Equals("__LINKEDIT"))
|
||||
{
|
||||
Verify(linkEdit == null, MachOFormatError.DuplicateLinkEdit);
|
||||
linkEdit = segment;
|
||||
}
|
||||
}
|
||||
else if (command->cmd == Command.LC_SYMTAB)
|
||||
{
|
||||
Verify(symtab == null, MachOFormatError.DuplicateSymtab);
|
||||
symtab = (SymtabCommand*)command;
|
||||
}
|
||||
|
||||
file += command->cmdsize;
|
||||
}
|
||||
|
||||
Verify(linkEdit != null, MachOFormatError.MissingLinkEdit);
|
||||
Verify(symtab != null, MachOFormatError.MissingSymtab);
|
||||
|
||||
// Update the string table to include bundle-data
|
||||
ulong newStringTableSize = fileLength - symtab->stroff;
|
||||
if (newStringTableSize > uint.MaxValue)
|
||||
{
|
||||
// Too big, too bad;
|
||||
return false;
|
||||
}
|
||||
symtab->strsize = (uint)newStringTableSize;
|
||||
|
||||
// Update the __LINKEDIT segment to include bundle-data
|
||||
linkEdit->filesize = fileLength - linkEdit->fileoff;
|
||||
linkEdit->vmsize = linkEdit->filesize;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (file != null)
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
180
src/SquirrelCli/HostModel/AppHost/PEUtils.cs
Normal file
180
src/SquirrelCli/HostModel/AppHost/PEUtils.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
|
||||
namespace Microsoft.NET.HostModel.AppHost
|
||||
{
|
||||
public static class PEUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// The first two bytes of a PE file are a constant signature.
|
||||
/// </summary>
|
||||
private const ushort PEFileSignature = 0x5A4D;
|
||||
|
||||
/// <summary>
|
||||
/// The offset of the PE header pointer in the DOS header.
|
||||
/// </summary>
|
||||
private const int PEHeaderPointerOffset = 0x3C;
|
||||
|
||||
/// <summary>
|
||||
/// The offset of the Subsystem field in the PE header.
|
||||
/// </summary>
|
||||
private const int SubsystemOffset = 0x5C;
|
||||
|
||||
/// <summary>
|
||||
/// The value of the sybsystem field which indicates Windows GUI (Graphical UI)
|
||||
/// </summary>
|
||||
private const ushort WindowsGUISubsystem = 0x2;
|
||||
|
||||
/// <summary>
|
||||
/// The value of the subsystem field which indicates Windows CUI (Console)
|
||||
/// </summary>
|
||||
private const ushort WindowsCUISubsystem = 0x3;
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the apphost file is a windows PE image by looking at the first few bytes.
|
||||
/// </summary>
|
||||
/// <param name="accessor">The memory accessor which has the apphost file opened.</param>
|
||||
/// <returns>true if the accessor represents a PE image, false otherwise.</returns>
|
||||
internal static unsafe bool IsPEImage(MemoryMappedViewAccessor accessor)
|
||||
{
|
||||
byte* pointer = null;
|
||||
|
||||
try
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
|
||||
byte* bytes = pointer + accessor.PointerOffset;
|
||||
|
||||
// https://en.wikipedia.org/wiki/Portable_Executable
|
||||
// Validate that we're looking at Windows PE file
|
||||
if (((ushort*)bytes)[0] != PEFileSignature || accessor.Capacity < PEHeaderPointerOffset + sizeof(uint))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pointer != null)
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsPEImage(string filePath)
|
||||
{
|
||||
using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
|
||||
{
|
||||
if (reader.BaseStream.Length < PEHeaderPointerOffset + sizeof(uint))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ushort signature = reader.ReadUInt16();
|
||||
return signature == PEFileSignature;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method will attempt to set the subsystem to GUI. The apphost file should be a windows PE file.
|
||||
/// </summary>
|
||||
/// <param name="accessor">The memory accessor which has the apphost file opened.</param>
|
||||
internal static unsafe void SetWindowsGraphicalUserInterfaceBit(MemoryMappedViewAccessor accessor)
|
||||
{
|
||||
byte* pointer = null;
|
||||
|
||||
try
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
|
||||
byte* bytes = pointer + accessor.PointerOffset;
|
||||
|
||||
// https://en.wikipedia.org/wiki/Portable_Executable
|
||||
uint peHeaderOffset = ((uint*)(bytes + PEHeaderPointerOffset))[0];
|
||||
|
||||
if (accessor.Capacity < peHeaderOffset + SubsystemOffset + sizeof(ushort))
|
||||
{
|
||||
throw new AppHostNotPEFileException();
|
||||
}
|
||||
|
||||
ushort* subsystem = ((ushort*)(bytes + peHeaderOffset + SubsystemOffset));
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/Debug/pe-format#windows-subsystem
|
||||
// The subsystem of the prebuilt apphost should be set to CUI
|
||||
if (subsystem[0] != WindowsCUISubsystem)
|
||||
{
|
||||
throw new AppHostNotCUIException();
|
||||
}
|
||||
|
||||
// Set the subsystem to GUI
|
||||
subsystem[0] = WindowsGUISubsystem;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pointer != null)
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe void SetWindowsGraphicalUserInterfaceBit(string filePath)
|
||||
{
|
||||
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
|
||||
{
|
||||
using (var accessor = mappedFile.CreateViewAccessor())
|
||||
{
|
||||
SetWindowsGraphicalUserInterfaceBit(accessor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method will return the subsystem CUI/GUI value. The apphost file should be a windows PE file.
|
||||
/// </summary>
|
||||
/// <param name="accessor">The memory accessor which has the apphost file opened.</param>
|
||||
internal static unsafe ushort GetWindowsGraphicalUserInterfaceBit(MemoryMappedViewAccessor accessor)
|
||||
{
|
||||
byte* pointer = null;
|
||||
|
||||
try
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
|
||||
byte* bytes = pointer + accessor.PointerOffset;
|
||||
|
||||
// https://en.wikipedia.org/wiki/Portable_Executable
|
||||
uint peHeaderOffset = ((uint*)(bytes + PEHeaderPointerOffset))[0];
|
||||
|
||||
if (accessor.Capacity < peHeaderOffset + SubsystemOffset + sizeof(ushort))
|
||||
{
|
||||
throw new AppHostNotPEFileException();
|
||||
}
|
||||
|
||||
ushort* subsystem = ((ushort*)(bytes + peHeaderOffset + SubsystemOffset));
|
||||
|
||||
return subsystem[0];
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pointer != null)
|
||||
{
|
||||
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe ushort GetWindowsGraphicalUserInterfaceBit(string filePath)
|
||||
{
|
||||
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
|
||||
{
|
||||
using (var accessor = mappedFile.CreateViewAccessor())
|
||||
{
|
||||
return GetWindowsGraphicalUserInterfaceBit(accessor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.NET.HostModel.AppHost
|
||||
{
|
||||
/// <summary>
|
||||
/// Unable to use input file as a valid application host executable, as it does not contain
|
||||
/// the expected placeholder byte sequence.
|
||||
/// </summary>
|
||||
public class PlaceHolderNotFoundInAppHostException : AppHostUpdateException
|
||||
{
|
||||
public byte[] MissingPattern { get; }
|
||||
public PlaceHolderNotFoundInAppHostException(byte[] pattern)
|
||||
{
|
||||
MissingPattern = pattern;
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/SquirrelCli/HostModel/AppHost/RetryUtil.cs
Normal file
98
src/SquirrelCli/HostModel/AppHost/RetryUtil.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.NET.HostModel
|
||||
{
|
||||
/// <summary>
|
||||
/// HostModel library implements several services for updating the AppHost DLL.
|
||||
/// These updates involve multiple file open/close operations.
|
||||
/// An Antivirus scanner may intercept in-between and lock the file,
|
||||
/// causing the operations to fail with IO-Error.
|
||||
/// So, the operations are retried a few times on failures such as
|
||||
/// - IOException
|
||||
/// - Failure with Win32 errors indicating file-lock
|
||||
/// </summary>
|
||||
public static class RetryUtil
|
||||
{
|
||||
public const int NumberOfRetries = 500;
|
||||
public const int NumMilliSecondsToWait = 100;
|
||||
|
||||
public static void RetryOnIOError(Action func)
|
||||
{
|
||||
for (int i = 1; i <= NumberOfRetries; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
func();
|
||||
break;
|
||||
}
|
||||
catch (IOException) when (i < NumberOfRetries)
|
||||
{
|
||||
Thread.Sleep(NumMilliSecondsToWait);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RetryOnWin32Error(Action func)
|
||||
{
|
||||
static bool IsKnownIrrecoverableError(int hresult)
|
||||
{
|
||||
// Error codes are defined in winerror.h
|
||||
// The error code is stored in the lowest 16 bits of the HResult
|
||||
|
||||
switch (hresult & 0xffff)
|
||||
{
|
||||
case 0x00000001: // ERROR_INVALID_FUNCTION
|
||||
case 0x00000002: // ERROR_FILE_NOT_FOUND
|
||||
case 0x00000003: // ERROR_PATH_NOT_FOUND
|
||||
case 0x00000006: // ERROR_INVALID_HANDLE
|
||||
case 0x00000008: // ERROR_NOT_ENOUGH_MEMORY
|
||||
case 0x0000000B: // ERROR_BAD_FORMAT
|
||||
case 0x0000000E: // ERROR_OUTOFMEMORY
|
||||
case 0x0000000F: // ERROR_INVALID_DRIVE
|
||||
case 0x00000012: // ERROR_NO_MORE_FILES
|
||||
case 0x00000035: // ERROR_BAD_NETPATH
|
||||
case 0x00000057: // ERROR_INVALID_PARAMETER
|
||||
case 0x00000071: // ERROR_NO_MORE_SEARCH_HANDLES
|
||||
case 0x00000072: // ERROR_INVALID_TARGET_HANDLE
|
||||
case 0x00000078: // ERROR_CALL_NOT_IMPLEMENTED
|
||||
case 0x0000007B: // ERROR_INVALID_NAME
|
||||
case 0x0000007C: // ERROR_INVALID_LEVEL
|
||||
case 0x0000007D: // ERROR_NO_VOLUME_LABEL
|
||||
case 0x0000009A: // ERROR_LABEL_TOO_LONG
|
||||
case 0x000000A0: // ERROR_BAD_ARGUMENTS
|
||||
case 0x000000A1: // ERROR_BAD_PATHNAME
|
||||
case 0x000000CE: // ERROR_FILENAME_EXCED_RANGE
|
||||
case 0x000000DF: // ERROR_FILE_TOO_LARGE
|
||||
case 0x000003ED: // ERROR_UNRECOGNIZED_VOLUME
|
||||
case 0x000003EE: // ERROR_FILE_INVALID
|
||||
case 0x00000651: // ERROR_DEVICE_REMOVED
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 1; i <= NumberOfRetries; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
func();
|
||||
break;
|
||||
}
|
||||
catch (HResultException hrex)
|
||||
when (i < NumberOfRetries && !IsKnownIrrecoverableError(hrex.Win32HResult))
|
||||
{
|
||||
Thread.Sleep(NumMilliSecondsToWait);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/SquirrelCli/HostModel/Bundle/BundleOptions.cs
Normal file
22
src/SquirrelCli/HostModel/Bundle/BundleOptions.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.NET.HostModel.Bundle
|
||||
{
|
||||
/// <summary>
|
||||
/// BundleOptions: Optional settings for configuring the type of files
|
||||
/// included in the single file bundle.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum BundleOptions
|
||||
{
|
||||
None = 0,
|
||||
BundleNativeBinaries = 1,
|
||||
BundleOtherFiles = 2,
|
||||
BundleSymbolFiles = 4,
|
||||
BundleAllContent = BundleNativeBinaries | BundleOtherFiles,
|
||||
EnableCompression = 8,
|
||||
};
|
||||
}
|
||||
342
src/SquirrelCli/HostModel/Bundle/Bundler.cs
Normal file
342
src/SquirrelCli/HostModel/Bundle/Bundler.cs
Normal file
@@ -0,0 +1,342 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Reflection.PortableExecutable;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.NET.HostModel.AppHost;
|
||||
|
||||
namespace Microsoft.NET.HostModel.Bundle
|
||||
{
|
||||
/// <summary>
|
||||
/// Bundler: Functionality to embed the managed app and its dependencies
|
||||
/// into the host native binary.
|
||||
/// </summary>
|
||||
public class Bundler
|
||||
{
|
||||
public const uint BundlerMajorVersion = 6;
|
||||
public const uint BundlerMinorVersion = 0;
|
||||
|
||||
private readonly string HostName;
|
||||
private readonly string OutputDir;
|
||||
private readonly string DepsJson;
|
||||
private readonly string RuntimeConfigJson;
|
||||
private readonly string RuntimeConfigDevJson;
|
||||
|
||||
private readonly Trace Tracer;
|
||||
public readonly Manifest BundleManifest;
|
||||
private readonly TargetInfo Target;
|
||||
private readonly BundleOptions Options;
|
||||
|
||||
public Bundler(string hostName,
|
||||
string outputDir,
|
||||
BundleOptions options = BundleOptions.None,
|
||||
OSPlatform? targetOS = null,
|
||||
Architecture? targetArch = null,
|
||||
Version targetFrameworkVersion = null,
|
||||
bool diagnosticOutput = false,
|
||||
string appAssemblyName = null)
|
||||
{
|
||||
Tracer = new Trace(diagnosticOutput);
|
||||
|
||||
HostName = hostName;
|
||||
OutputDir = Path.GetFullPath(string.IsNullOrEmpty(outputDir) ? Environment.CurrentDirectory : outputDir);
|
||||
Target = new TargetInfo(targetOS, targetArch, targetFrameworkVersion);
|
||||
|
||||
if (Target.BundleMajorVersion < 6 &&
|
||||
(options & BundleOptions.EnableCompression) != 0)
|
||||
{
|
||||
throw new ArgumentException("Compression requires framework version 6.0 or above", nameof(options));
|
||||
}
|
||||
|
||||
appAssemblyName ??= Target.GetAssemblyName(hostName);
|
||||
DepsJson = appAssemblyName + ".deps.json";
|
||||
RuntimeConfigJson = appAssemblyName + ".runtimeconfig.json";
|
||||
RuntimeConfigDevJson = appAssemblyName + ".runtimeconfig.dev.json";
|
||||
|
||||
BundleManifest = new Manifest(Target.BundleMajorVersion, netcoreapp3CompatMode: options.HasFlag(BundleOptions.BundleAllContent));
|
||||
Options = Target.DefaultOptions | options;
|
||||
}
|
||||
|
||||
private bool ShouldCompress(FileType type)
|
||||
{
|
||||
if (!Options.HasFlag(BundleOptions.EnableCompression))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case FileType.DepsJson:
|
||||
case FileType.RuntimeConfigJson:
|
||||
return false;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Embed 'file' into 'bundle'
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// startOffset: offset of the start 'file' within 'bundle'
|
||||
/// compressedSize: size of the compressed data, if entry was compressed, otherwise 0
|
||||
/// </returns>
|
||||
private (long startOffset, long compressedSize) AddToBundle(Stream bundle, Stream file, FileType type)
|
||||
{
|
||||
long startOffset = bundle.Position;
|
||||
if (ShouldCompress(type))
|
||||
{
|
||||
long fileLength = file.Length;
|
||||
file.Position = 0;
|
||||
|
||||
// We use DeflateStream here.
|
||||
// It uses GZip algorithm, but with a trivial header that does not contain file info.
|
||||
using (DeflateStream compressionStream = new DeflateStream(bundle, CompressionLevel.Optimal, leaveOpen: true))
|
||||
{
|
||||
file.CopyTo(compressionStream);
|
||||
}
|
||||
|
||||
long compressedSize = bundle.Position - startOffset;
|
||||
if (compressedSize < fileLength * 0.75)
|
||||
{
|
||||
return (startOffset, compressedSize);
|
||||
}
|
||||
|
||||
// compression rate was not good enough
|
||||
// roll back the bundle offset and let the uncompressed code path take care of the entry.
|
||||
bundle.Seek(startOffset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
if (type == FileType.Assembly)
|
||||
{
|
||||
long misalignment = (bundle.Position % Target.AssemblyAlignment);
|
||||
|
||||
if (misalignment != 0)
|
||||
{
|
||||
long padding = Target.AssemblyAlignment - misalignment;
|
||||
bundle.Position += padding;
|
||||
}
|
||||
}
|
||||
|
||||
file.Position = 0;
|
||||
startOffset = bundle.Position;
|
||||
file.CopyTo(bundle);
|
||||
|
||||
return (startOffset, 0);
|
||||
}
|
||||
|
||||
private bool IsHost(string fileRelativePath)
|
||||
{
|
||||
return fileRelativePath.Equals(HostName);
|
||||
}
|
||||
|
||||
private bool ShouldIgnore(string fileRelativePath)
|
||||
{
|
||||
return fileRelativePath.Equals(RuntimeConfigDevJson);
|
||||
}
|
||||
|
||||
private bool ShouldExclude(FileType type, string relativePath)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case FileType.Assembly:
|
||||
case FileType.DepsJson:
|
||||
case FileType.RuntimeConfigJson:
|
||||
return false;
|
||||
|
||||
case FileType.NativeBinary:
|
||||
return !Options.HasFlag(BundleOptions.BundleNativeBinaries) || Target.ShouldExclude(relativePath);
|
||||
|
||||
case FileType.Symbols:
|
||||
return !Options.HasFlag(BundleOptions.BundleSymbolFiles);
|
||||
|
||||
case FileType.Unknown:
|
||||
return !Options.HasFlag(BundleOptions.BundleOtherFiles);
|
||||
|
||||
default:
|
||||
Debug.Assert(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsAssembly(string path, out bool isPE)
|
||||
{
|
||||
isPE = false;
|
||||
|
||||
using (FileStream file = File.OpenRead(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
PEReader peReader = new PEReader(file);
|
||||
CorHeader corHeader = peReader.PEHeaders.CorHeader;
|
||||
|
||||
isPE = true; // If peReader.PEHeaders doesn't throw, it is a valid PEImage
|
||||
return corHeader != null;
|
||||
}
|
||||
catch (BadImageFormatException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private FileType InferType(FileSpec fileSpec)
|
||||
{
|
||||
if (fileSpec.BundleRelativePath.Equals(DepsJson))
|
||||
{
|
||||
return FileType.DepsJson;
|
||||
}
|
||||
|
||||
if (fileSpec.BundleRelativePath.Equals(RuntimeConfigJson))
|
||||
{
|
||||
return FileType.RuntimeConfigJson;
|
||||
}
|
||||
|
||||
if (Path.GetExtension(fileSpec.BundleRelativePath).ToLowerInvariant().Equals(".pdb"))
|
||||
{
|
||||
return FileType.Symbols;
|
||||
}
|
||||
|
||||
bool isPE;
|
||||
if (IsAssembly(fileSpec.SourcePath, out isPE))
|
||||
{
|
||||
return FileType.Assembly;
|
||||
}
|
||||
|
||||
bool isNativeBinary = Target.IsWindows ? isPE : Target.IsNativeBinary(fileSpec.SourcePath);
|
||||
|
||||
if (isNativeBinary)
|
||||
{
|
||||
return FileType.NativeBinary;
|
||||
}
|
||||
|
||||
return FileType.Unknown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a bundle, given the specification of embedded files
|
||||
/// </summary>
|
||||
/// <param name="fileSpecs">
|
||||
/// An enumeration FileSpecs for the files to be embedded.
|
||||
///
|
||||
/// Files in fileSpecs that are not bundled within the single file bundle,
|
||||
/// and should be published as separate files are marked as "IsExcluded" by this method.
|
||||
/// This doesn't include unbundled files that should be dropped, and not publised as output.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The full path the the generated bundle file
|
||||
/// </returns>
|
||||
/// <exceptions>
|
||||
/// ArgumentException if input is invalid
|
||||
/// IOExceptions and ArgumentExceptions from callees flow to the caller.
|
||||
/// </exceptions>
|
||||
public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
|
||||
{
|
||||
Tracer.Log($"Bundler Version: {BundlerMajorVersion}.{BundlerMinorVersion}");
|
||||
Tracer.Log($"Bundle Version: {BundleManifest.BundleVersion}");
|
||||
Tracer.Log($"Target Runtime: {Target}");
|
||||
Tracer.Log($"Bundler Options: {Options}");
|
||||
|
||||
if (fileSpecs.Any(x => !x.IsValid()))
|
||||
{
|
||||
throw new ArgumentException("Invalid input specification: Found entry with empty source-path or bundle-relative-path.");
|
||||
}
|
||||
|
||||
string hostSource;
|
||||
try
|
||||
{
|
||||
hostSource = fileSpecs.Where(x => x.BundleRelativePath.Equals(HostName)).Single().SourcePath;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
throw new ArgumentException("Invalid input specification: Must specify the host binary");
|
||||
}
|
||||
|
||||
string bundlePath = Path.Combine(OutputDir, HostName);
|
||||
if (File.Exists(bundlePath))
|
||||
{
|
||||
Tracer.Log($"Ovewriting existing File {bundlePath}");
|
||||
}
|
||||
|
||||
BinaryUtils.CopyFile(hostSource, bundlePath);
|
||||
|
||||
// Note: We're comparing file paths both on the OS we're running on as well as on the target OS for the app
|
||||
// We can't really make assumptions about the file systems (even on Linux there can be case insensitive file systems
|
||||
// and vice versa for Windows). So it's safer to do case sensitive comparison everywhere.
|
||||
var relativePathToSpec = new Dictionary<string, FileSpec>(StringComparer.Ordinal);
|
||||
|
||||
long headerOffset = 0;
|
||||
using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(bundlePath)))
|
||||
{
|
||||
Stream bundle = writer.BaseStream;
|
||||
bundle.Position = bundle.Length;
|
||||
|
||||
foreach (var fileSpec in fileSpecs)
|
||||
{
|
||||
string relativePath = fileSpec.BundleRelativePath;
|
||||
|
||||
if (IsHost(relativePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ShouldIgnore(relativePath))
|
||||
{
|
||||
Tracer.Log($"Ignore: {relativePath}");
|
||||
continue;
|
||||
}
|
||||
|
||||
FileType type = InferType(fileSpec);
|
||||
|
||||
if (ShouldExclude(type, relativePath))
|
||||
{
|
||||
Tracer.Log($"Exclude [{type}]: {relativePath}");
|
||||
fileSpec.Excluded = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (relativePathToSpec.TryGetValue(fileSpec.BundleRelativePath, out var existingFileSpec))
|
||||
{
|
||||
if (!string.Equals(fileSpec.SourcePath, existingFileSpec.SourcePath, StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException($"Invalid input specification: Found entries '{fileSpec.SourcePath}' and '{existingFileSpec.SourcePath}' with the same BundleRelativePath '{fileSpec.BundleRelativePath}'");
|
||||
}
|
||||
|
||||
// Exact duplicate - intentionally skip and don't include a second copy in the bundle
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
relativePathToSpec.Add(fileSpec.BundleRelativePath, fileSpec);
|
||||
}
|
||||
|
||||
using (FileStream file = File.OpenRead(fileSpec.SourcePath))
|
||||
{
|
||||
FileType targetType = Target.TargetSpecificFileType(type);
|
||||
(long startOffset, long compressedSize) = AddToBundle(bundle, file, targetType);
|
||||
FileEntry entry = BundleManifest.AddEntry(targetType, file, relativePath, startOffset, compressedSize, Target.BundleMajorVersion);
|
||||
Tracer.Log($"Embed: {entry}");
|
||||
}
|
||||
}
|
||||
|
||||
// Write the bundle manifest
|
||||
headerOffset = BundleManifest.Write(writer);
|
||||
Tracer.Log($"Header Offset={headerOffset}");
|
||||
Tracer.Log($"Meta-data Size={writer.BaseStream.Position - headerOffset}");
|
||||
Tracer.Log($"Bundle: Path={bundlePath}, Size={bundle.Length}");
|
||||
}
|
||||
|
||||
HostWriter.SetAsBundle(bundlePath, headerOffset);
|
||||
|
||||
return bundlePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/SquirrelCli/HostModel/Bundle/FileEntry.cs
Normal file
58
src/SquirrelCli/HostModel/Bundle/FileEntry.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.NET.HostModel.Bundle
|
||||
{
|
||||
/// <summary>
|
||||
/// FileEntry: Records information about embedded files.
|
||||
///
|
||||
/// The bundle manifest records the following meta-data for each
|
||||
/// file embedded in the bundle:
|
||||
/// * Type (1 byte)
|
||||
/// * NameLength (7-bit extension encoding, typically 1 byte)
|
||||
/// * Name ("NameLength" Bytes)
|
||||
/// * Offset (Int64)
|
||||
/// * Size (Int64)
|
||||
/// === present only in bundle version 3+
|
||||
/// * CompressedSize (Int64) 0 indicates No Compression
|
||||
/// </summary>
|
||||
public class FileEntry
|
||||
{
|
||||
public readonly uint BundleMajorVersion;
|
||||
|
||||
public readonly long Offset;
|
||||
public readonly long Size;
|
||||
public readonly long CompressedSize;
|
||||
public readonly FileType Type;
|
||||
public readonly string RelativePath; // Path of an embedded file, relative to the Bundle source-directory.
|
||||
|
||||
public const char DirectorySeparatorChar = '/';
|
||||
|
||||
public FileEntry(FileType fileType, string relativePath, long offset, long size, long compressedSize, uint bundleMajorVersion)
|
||||
{
|
||||
BundleMajorVersion = bundleMajorVersion;
|
||||
Type = fileType;
|
||||
RelativePath = relativePath.Replace('\\', DirectorySeparatorChar);
|
||||
Offset = offset;
|
||||
Size = size;
|
||||
CompressedSize = compressedSize;
|
||||
}
|
||||
|
||||
public void Write(BinaryWriter writer)
|
||||
{
|
||||
writer.Write(Offset);
|
||||
writer.Write(Size);
|
||||
// compression is used only in version 6.0+
|
||||
if (BundleMajorVersion >= 6)
|
||||
{
|
||||
writer.Write(CompressedSize);
|
||||
}
|
||||
writer.Write((byte)Type);
|
||||
writer.Write(RelativePath);
|
||||
}
|
||||
|
||||
public override string ToString() => $"{RelativePath} [{Type}] @{Offset} Sz={Size} CompressedSz={CompressedSize}";
|
||||
}
|
||||
}
|
||||
36
src/SquirrelCli/HostModel/Bundle/FileSpec.cs
Normal file
36
src/SquirrelCli/HostModel/Bundle/FileSpec.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.NET.HostModel.Bundle
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about files to embed into the Bundle (input to the Bundler).
|
||||
///
|
||||
/// SourcePath: path to the file to be bundled at compile time
|
||||
/// BundleRelativePath: path where the file is expected at run time,
|
||||
/// relative to the app DLL.
|
||||
/// </summary>
|
||||
public class FileSpec
|
||||
{
|
||||
public readonly string SourcePath;
|
||||
public readonly string BundleRelativePath;
|
||||
public bool Excluded;
|
||||
|
||||
public FileSpec(string sourcePath, string bundleRelativePath)
|
||||
{
|
||||
SourcePath = sourcePath;
|
||||
BundleRelativePath = bundleRelativePath;
|
||||
Excluded = false;
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(SourcePath) &&
|
||||
!string.IsNullOrWhiteSpace(BundleRelativePath);
|
||||
}
|
||||
|
||||
public override string ToString() => $"SourcePath: {SourcePath}, RelativePath: {BundleRelativePath} {(Excluded ? "[Excluded]" : "")}";
|
||||
}
|
||||
}
|
||||
21
src/SquirrelCli/HostModel/Bundle/FileType.cs
Normal file
21
src/SquirrelCli/HostModel/Bundle/FileType.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.NET.HostModel.Bundle
|
||||
{
|
||||
/// <summary>
|
||||
/// FileType: Identifies the type of file embedded into the bundle.
|
||||
///
|
||||
/// The bundler differentiates a few kinds of files via the manifest,
|
||||
/// with respect to the way in which they'll be used by the runtime.
|
||||
/// </summary>
|
||||
public enum FileType : byte
|
||||
{
|
||||
Unknown, // Type not determined.
|
||||
Assembly, // IL and R2R Assemblies
|
||||
NativeBinary, // NativeBinaries
|
||||
DepsJson, // .deps.json configuration file
|
||||
RuntimeConfigJson, // .runtimeconfig.json configuration file
|
||||
Symbols // PDB Files
|
||||
};
|
||||
}
|
||||
177
src/SquirrelCli/HostModel/Bundle/Manifest.cs
Normal file
177
src/SquirrelCli/HostModel/Bundle/Manifest.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Microsoft.NET.HostModel.Bundle
|
||||
{
|
||||
/// <summary>
|
||||
/// BundleManifest is a description of the contents of a bundle file.
|
||||
/// This class handles creation and consumption of bundle-manifests.
|
||||
///
|
||||
/// Here is the description of the Bundle Layout:
|
||||
/// _______________________________________________
|
||||
/// AppHost
|
||||
///
|
||||
///
|
||||
/// ------------Embedded Files ---------------------
|
||||
/// The embedded files including the app, its
|
||||
/// configuration files, dependencies, and
|
||||
/// possibly the runtime.
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
/// ------------ Bundle Header -------------
|
||||
/// MajorVersion
|
||||
/// MinorVersion
|
||||
/// NumEmbeddedFiles
|
||||
/// ExtractionID
|
||||
/// DepsJson Location [Version 2+]
|
||||
/// Offset
|
||||
/// Size
|
||||
/// RuntimeConfigJson Location [Version 2+]
|
||||
/// Offset
|
||||
/// Size
|
||||
/// Flags [Version 2+]
|
||||
/// - - - - - - Manifest Entries - - - - - - - - - - -
|
||||
/// Series of FileEntries (for each embedded file)
|
||||
/// [File Type, Name, Offset, Size information]
|
||||
///
|
||||
///
|
||||
///
|
||||
/// _________________________________________________
|
||||
/// </summary>
|
||||
public class Manifest
|
||||
{
|
||||
// NetcoreApp3CompatMode flag is set on a .net5 app,
|
||||
// which chooses to build single-file apps in .netcore3.x compat mode,
|
||||
// by constructing the bundler with BundleAllConent option.
|
||||
// This mode is expected to be deprecated in future versions of .NET.
|
||||
[Flags]
|
||||
private enum HeaderFlags : ulong
|
||||
{
|
||||
None = 0,
|
||||
NetcoreApp3CompatMode = 1
|
||||
}
|
||||
|
||||
// Bundle ID is a string that is used to uniquely
|
||||
// identify this bundle. It is choosen to be compatible
|
||||
// with path-names so that the AppHost can use it in
|
||||
// extraction path.
|
||||
public string BundleID { get; private set; }
|
||||
//Same as Path.GetRandomFileName
|
||||
private const int BundleIdLength = 12;
|
||||
private SHA256 bundleHash = SHA256.Create();
|
||||
public readonly uint BundleMajorVersion;
|
||||
// The Minor version is currently unused, and is always zero
|
||||
public const uint BundleMinorVersion = 0;
|
||||
private FileEntry DepsJsonEntry;
|
||||
private FileEntry RuntimeConfigJsonEntry;
|
||||
private HeaderFlags Flags;
|
||||
public List<FileEntry> Files;
|
||||
public string BundleVersion => $"{BundleMajorVersion}.{BundleMinorVersion}";
|
||||
|
||||
public Manifest(uint bundleMajorVersion, bool netcoreapp3CompatMode = false)
|
||||
{
|
||||
BundleMajorVersion = bundleMajorVersion;
|
||||
Files = new List<FileEntry>();
|
||||
Flags = (netcoreapp3CompatMode) ? HeaderFlags.NetcoreApp3CompatMode : HeaderFlags.None;
|
||||
}
|
||||
|
||||
public FileEntry AddEntry(FileType type, FileStream fileContent, string relativePath, long offset, long compressedSize, uint bundleMajorVersion)
|
||||
{
|
||||
if (bundleHash == null)
|
||||
{
|
||||
throw new InvalidOperationException("It is forbidden to change Manifest state after it was written or BundleId was obtained.");
|
||||
}
|
||||
|
||||
FileEntry entry = new FileEntry(type, relativePath, offset, fileContent.Length, compressedSize, bundleMajorVersion);
|
||||
Files.Add(entry);
|
||||
|
||||
fileContent.Position = 0;
|
||||
byte[] hashBytes = ComputeSha256Hash(fileContent);
|
||||
bundleHash.TransformBlock(hashBytes, 0, hashBytes.Length, hashBytes, 0);
|
||||
|
||||
switch (entry.Type)
|
||||
{
|
||||
case FileType.DepsJson:
|
||||
DepsJsonEntry = entry;
|
||||
break;
|
||||
case FileType.RuntimeConfigJson:
|
||||
RuntimeConfigJsonEntry = entry;
|
||||
break;
|
||||
|
||||
case FileType.Assembly:
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static byte[] ComputeSha256Hash(Stream stream)
|
||||
{
|
||||
using (SHA256 sha = SHA256.Create())
|
||||
{
|
||||
return sha.ComputeHash(stream);
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateDeterministicId()
|
||||
{
|
||||
bundleHash.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
|
||||
byte[] manifestHash = bundleHash.Hash;
|
||||
bundleHash.Dispose();
|
||||
bundleHash = null;
|
||||
|
||||
return Convert.ToBase64String(manifestHash).Substring(BundleIdLength).Replace('/', '_');
|
||||
}
|
||||
|
||||
public long Write(BinaryWriter writer)
|
||||
{
|
||||
BundleID = BundleID ?? GenerateDeterministicId();
|
||||
|
||||
long startOffset = writer.BaseStream.Position;
|
||||
|
||||
// Write the bundle header
|
||||
writer.Write(BundleMajorVersion);
|
||||
writer.Write(BundleMinorVersion);
|
||||
writer.Write(Files.Count);
|
||||
writer.Write(BundleID);
|
||||
|
||||
if (BundleMajorVersion >= 2)
|
||||
{
|
||||
writer.Write((DepsJsonEntry != null) ? DepsJsonEntry.Offset : 0);
|
||||
writer.Write((DepsJsonEntry != null) ? DepsJsonEntry.Size : 0);
|
||||
|
||||
writer.Write((RuntimeConfigJsonEntry != null) ? RuntimeConfigJsonEntry.Offset : 0);
|
||||
writer.Write((RuntimeConfigJsonEntry != null) ? RuntimeConfigJsonEntry.Size : 0);
|
||||
|
||||
writer.Write((ulong)Flags);
|
||||
}
|
||||
|
||||
// Write the manifest entries
|
||||
foreach (FileEntry entry in Files)
|
||||
{
|
||||
entry.Write(writer);
|
||||
}
|
||||
|
||||
return startOffset;
|
||||
}
|
||||
|
||||
public bool Contains(string relativePath)
|
||||
{
|
||||
return Files.Any(entry => relativePath.Equals(entry.RelativePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
120
src/SquirrelCli/HostModel/Bundle/TargetInfo.cs
Normal file
120
src/SquirrelCli/HostModel/Bundle/TargetInfo.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.NET.HostModel.AppHost;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.NET.HostModel.Bundle
|
||||
{
|
||||
/// <summary>
|
||||
/// TargetInfo: Information about the target for which the single-file bundle is built.
|
||||
///
|
||||
/// Currently the TargetInfo only tracks:
|
||||
/// - the target operating system
|
||||
/// - the target architecture
|
||||
/// - the target framework
|
||||
/// - the default options for this target
|
||||
/// - the assembly alignment for this target
|
||||
/// </summary>
|
||||
|
||||
public class TargetInfo
|
||||
{
|
||||
public readonly OSPlatform OS;
|
||||
public readonly Architecture Arch;
|
||||
public readonly Version FrameworkVersion;
|
||||
public readonly uint BundleMajorVersion;
|
||||
public readonly BundleOptions DefaultOptions;
|
||||
public readonly int AssemblyAlignment;
|
||||
|
||||
public TargetInfo(OSPlatform? os, Architecture? arch, Version targetFrameworkVersion)
|
||||
{
|
||||
OS = os ?? HostOS;
|
||||
Arch = arch ?? RuntimeInformation.OSArchitecture;
|
||||
FrameworkVersion = targetFrameworkVersion ?? net60;
|
||||
|
||||
Debug.Assert(IsLinux || IsOSX || IsWindows);
|
||||
|
||||
if (FrameworkVersion.CompareTo(net60) >= 0)
|
||||
{
|
||||
BundleMajorVersion = 6u;
|
||||
DefaultOptions = BundleOptions.None;
|
||||
}
|
||||
else if (FrameworkVersion.CompareTo(net50) >= 0)
|
||||
{
|
||||
BundleMajorVersion = 2u;
|
||||
DefaultOptions = BundleOptions.None;
|
||||
}
|
||||
else if (FrameworkVersion.Major == 3 && (FrameworkVersion.Minor == 0 || FrameworkVersion.Minor == 1))
|
||||
{
|
||||
BundleMajorVersion = 1u;
|
||||
DefaultOptions = BundleOptions.BundleAllContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Invalid input: Unsupported Target Framework Version {targetFrameworkVersion}");
|
||||
}
|
||||
|
||||
if (IsLinux && Arch == Architecture.Arm64)
|
||||
{
|
||||
// We align assemblies in the bundle at 4K so that we can use mmap on Linux without changing the page alignment of ARM64 R2R code.
|
||||
// This is only necessary for R2R assemblies, but we do it for all assemblies for simplicity.
|
||||
// See https://github.com/dotnet/runtime/issues/41832.
|
||||
AssemblyAlignment = 4096;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, assemblies are 16 bytes aligned, so that their sections can be memory-mapped cache aligned.
|
||||
AssemblyAlignment = 16;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsNativeBinary(string filePath)
|
||||
{
|
||||
return IsLinux ? ElfUtils.IsElfImage(filePath) : IsOSX ? MachOUtils.IsMachOImage(filePath) : PEUtils.IsPEImage(filePath);
|
||||
}
|
||||
|
||||
public string GetAssemblyName(string hostName)
|
||||
{
|
||||
// This logic to calculate assembly name from hostName should be removed (and probably moved to test helpers)
|
||||
// once the SDK in the correct assembly name.
|
||||
return (IsWindows ? Path.GetFileNameWithoutExtension(hostName) : hostName);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string os = IsWindows ? "win" : IsLinux ? "linux" : "osx";
|
||||
string arch = Arch.ToString().ToLowerInvariant();
|
||||
return $"OS: {os} Arch: {arch} FrameworkVersion: {FrameworkVersion}";
|
||||
}
|
||||
|
||||
private static OSPlatform HostOS => RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? OSPlatform.Linux :
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? OSPlatform.OSX : OSPlatform.Windows;
|
||||
|
||||
public bool IsLinux => OS.Equals(OSPlatform.Linux);
|
||||
public bool IsOSX => OS.Equals(OSPlatform.OSX);
|
||||
public bool IsWindows => OS.Equals(OSPlatform.Windows);
|
||||
|
||||
// The .net core 3 apphost doesn't care about semantics of FileType -- all files are extracted at startup.
|
||||
// However, the apphost checks that the FileType value is within expected bounds, so set it to the first enumeration.
|
||||
public FileType TargetSpecificFileType(FileType fileType) => (BundleMajorVersion == 1) ? FileType.Unknown : fileType;
|
||||
|
||||
// In .net core 3.x, bundle processing happens within the AppHost.
|
||||
// Therefore HostFxr and HostPolicy can be bundled within the single-file app.
|
||||
// In .net 5, bundle processing happens in HostFxr and HostPolicy libraries.
|
||||
// Therefore, these libraries themselves cannot be bundled into the single-file app.
|
||||
// This problem is mitigated by statically linking these host components with the AppHost.
|
||||
// https://github.com/dotnet/runtime/issues/32823
|
||||
public bool ShouldExclude(string relativePath) =>
|
||||
(FrameworkVersion.Major != 3) && (relativePath.Equals(HostFxr) || relativePath.Equals(HostPolicy));
|
||||
|
||||
private readonly Version net60 = new Version(6, 0);
|
||||
private readonly Version net50 = new Version(5, 0);
|
||||
private string HostFxr => IsWindows ? "hostfxr.dll" : IsLinux ? "libhostfxr.so" : "libhostfxr.dylib";
|
||||
private string HostPolicy => IsWindows ? "hostpolicy.dll" : IsLinux ? "libhostpolicy.so" : "libhostpolicy.dylib";
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
33
src/SquirrelCli/HostModel/Bundle/Trace.cs
Normal file
33
src/SquirrelCli/HostModel/Bundle/Trace.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.NET.HostModel.Bundle
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracing utilities for diagnostic output
|
||||
/// </summary>
|
||||
public class Trace
|
||||
{
|
||||
private readonly bool Verbose;
|
||||
|
||||
public Trace(bool verbose)
|
||||
{
|
||||
Verbose = verbose;
|
||||
}
|
||||
|
||||
public void Log(string fmt, params object[] args)
|
||||
{
|
||||
if (Verbose)
|
||||
{
|
||||
Console.WriteLine("LOG: " + fmt, args);
|
||||
}
|
||||
}
|
||||
|
||||
public void Error(string type, string message)
|
||||
{
|
||||
Console.Error.WriteLine($"ERROR: {message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/SquirrelCli/HostModel/README.md
Normal file
4
src/SquirrelCli/HostModel/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# HostModel
|
||||
|
||||
The code in this folder is taken from dotnet/runtime which is MIT licensed.
|
||||
https://github.com/dotnet/runtime/tree/main/src/installer/managed/Microsoft.NET.HostModel
|
||||
491
src/SquirrelCli/HostModel/ResourceUpdater.cs
Normal file
491
src/SquirrelCli/HostModel/ResourceUpdater.cs
Normal file
@@ -0,0 +1,491 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.NET.HostModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods for modifying the embedded native resources
|
||||
/// in a PE image. It currently only works on Windows, because it
|
||||
/// requires various kernel32 APIs.
|
||||
/// </summary>
|
||||
public class ResourceUpdater : IDisposable
|
||||
{
|
||||
private sealed class Kernel32
|
||||
{
|
||||
//
|
||||
// Native methods for updating resources
|
||||
//
|
||||
|
||||
[DllImport(nameof(Kernel32), CharSet = CharSet.Unicode, SetLastError=true)]
|
||||
public static extern SafeUpdateHandle BeginUpdateResource(string pFileName,
|
||||
[MarshalAs(UnmanagedType.Bool)]bool bDeleteExistingResources);
|
||||
|
||||
// Update a resource with data from an IntPtr
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool UpdateResource(SafeUpdateHandle hUpdate,
|
||||
IntPtr lpType,
|
||||
IntPtr lpName,
|
||||
ushort wLanguage,
|
||||
IntPtr lpData,
|
||||
uint cbData);
|
||||
|
||||
// Update a resource with data from a managed byte[]
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool UpdateResource(SafeUpdateHandle hUpdate,
|
||||
IntPtr lpType,
|
||||
IntPtr lpName,
|
||||
ushort wLanguage,
|
||||
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex=5)] byte[] lpData,
|
||||
uint cbData);
|
||||
|
||||
// Update a resource with data from a managed byte[]
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool UpdateResource(SafeUpdateHandle hUpdate,
|
||||
string lpType,
|
||||
IntPtr lpName,
|
||||
ushort wLanguage,
|
||||
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex=5)] byte[] lpData,
|
||||
uint cbData);
|
||||
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool EndUpdateResource(SafeUpdateHandle hUpdate,
|
||||
bool fDiscard);
|
||||
|
||||
// The IntPtr version of this dllimport is used in the
|
||||
// SafeHandle implementation
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool EndUpdateResource(IntPtr hUpdate,
|
||||
bool fDiscard);
|
||||
|
||||
public const ushort LangID_LangNeutral_SublangNeutral = 0;
|
||||
|
||||
//
|
||||
// Native methods used to read resources from a PE file
|
||||
//
|
||||
|
||||
// Loading and freeing PE files
|
||||
|
||||
public enum LoadLibraryFlags : uint
|
||||
{
|
||||
LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE = 0x00000040,
|
||||
LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x00000020
|
||||
}
|
||||
|
||||
[DllImport(nameof(Kernel32), CharSet = CharSet.Unicode, SetLastError=true)]
|
||||
public static extern IntPtr LoadLibraryEx(string lpFileName,
|
||||
IntPtr hReservedNull,
|
||||
LoadLibraryFlags dwFlags);
|
||||
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool FreeLibrary(IntPtr hModule);
|
||||
|
||||
// Enumerating resources
|
||||
|
||||
public delegate bool EnumResTypeProc(IntPtr hModule,
|
||||
IntPtr lpType,
|
||||
IntPtr lParam);
|
||||
|
||||
public delegate bool EnumResNameProc(IntPtr hModule,
|
||||
IntPtr lpType,
|
||||
IntPtr lpName,
|
||||
IntPtr lParam);
|
||||
|
||||
public delegate bool EnumResLangProc(IntPtr hModule,
|
||||
IntPtr lpType,
|
||||
IntPtr lpName,
|
||||
ushort wLang,
|
||||
IntPtr lParam);
|
||||
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool EnumResourceTypes(IntPtr hModule,
|
||||
EnumResTypeProc lpEnumFunc,
|
||||
IntPtr lParam);
|
||||
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool EnumResourceNames(IntPtr hModule,
|
||||
IntPtr lpType,
|
||||
EnumResNameProc lpEnumFunc,
|
||||
IntPtr lParam);
|
||||
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool EnumResourceLanguages(IntPtr hModule,
|
||||
IntPtr lpType,
|
||||
IntPtr lpName,
|
||||
EnumResLangProc lpEnumFunc,
|
||||
IntPtr lParam);
|
||||
|
||||
public const int UserStoppedResourceEnumerationHRESULT = unchecked((int)0x80073B02);
|
||||
public const int ResourceDataNotFoundHRESULT = unchecked((int)0x80070714);
|
||||
|
||||
// Querying and loading resources
|
||||
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
public static extern IntPtr FindResourceEx(IntPtr hModule,
|
||||
IntPtr lpType,
|
||||
IntPtr lpName,
|
||||
ushort wLanguage);
|
||||
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
public static extern IntPtr LoadResource(IntPtr hModule,
|
||||
IntPtr hResInfo);
|
||||
|
||||
[DllImport(nameof(Kernel32))] // does not call SetLastError
|
||||
public static extern IntPtr LockResource(IntPtr hResData);
|
||||
|
||||
[DllImport(nameof(Kernel32), SetLastError=true)]
|
||||
public static extern uint SizeofResource(IntPtr hModule,
|
||||
IntPtr hResInfo);
|
||||
|
||||
public const int ERROR_CALL_NOT_IMPLEMENTED = 0x78;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the update handle returned by BeginUpdateResource.
|
||||
/// Normally, native resources for the update handle are
|
||||
/// released by a call to ResourceUpdater.Update(). In case
|
||||
/// this doesn't happen, the SafeUpdateHandle will release the
|
||||
/// native resources for the update handle without updating
|
||||
/// the target file.
|
||||
/// </summary>
|
||||
private sealed class SafeUpdateHandle : SafeHandle
|
||||
{
|
||||
public SafeUpdateHandle() : base(IntPtr.Zero, true)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool IsInvalid => handle == IntPtr.Zero;
|
||||
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
// discard pending updates without writing them
|
||||
return Kernel32.EndUpdateResource(handle, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the native handle for the resource update.
|
||||
/// </summary>
|
||||
private readonly SafeUpdateHandle hUpdate;
|
||||
|
||||
///<summary>
|
||||
/// Determines if the ResourceUpdater is supported by the current operating system.
|
||||
/// Some versions of Windows, such as Nano Server, do not support the needed APIs.
|
||||
/// </summary>
|
||||
public static bool IsSupportedOS()
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// On Nano Server 1709+, `BeginUpdateResource` is exported but returns a null handle with a zero error
|
||||
// Try to call `BeginUpdateResource` with an invalid parameter; the error should be non-zero if supported
|
||||
// On Nano Server 20213, `BeginUpdateResource` fails with ERROR_CALL_NOT_IMPLEMENTED
|
||||
using (var handle = Kernel32.BeginUpdateResource("", false))
|
||||
{
|
||||
int lastWin32Error = Marshal.GetLastWin32Error();
|
||||
|
||||
if (handle.IsInvalid && (lastWin32Error == 0 || lastWin32Error == Kernel32.ERROR_CALL_NOT_IMPLEMENTED))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (EntryPointNotFoundException)
|
||||
{
|
||||
// BeginUpdateResource isn't exported from Kernel32
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a resource updater for the given PE file. This will
|
||||
/// acquire a native resource update handle for the file,
|
||||
/// preparing it for updates. Resources can be added to this
|
||||
/// updater, which will queue them for update. The target PE
|
||||
/// file will not be modified until Update() is called, after
|
||||
/// which the ResourceUpdater can not be used for further
|
||||
/// updates.
|
||||
/// </summary>
|
||||
public ResourceUpdater(string peFile)
|
||||
{
|
||||
hUpdate = Kernel32.BeginUpdateResource(peFile, false);
|
||||
if (hUpdate.IsInvalid)
|
||||
{
|
||||
ThrowExceptionForLastWin32Error();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add all resources from a source PE file. It is assumed
|
||||
/// that the input is a valid PE file. If it is not, an
|
||||
/// exception will be thrown. This will not modify the target
|
||||
/// until Update() is called.
|
||||
/// Throws an InvalidOperationException if Update() was already called.
|
||||
/// </summary>
|
||||
public ResourceUpdater AddResourcesFromPEImage(string peFile)
|
||||
{
|
||||
if (hUpdate.IsInvalid)
|
||||
{
|
||||
ThrowExceptionForInvalidUpdate();
|
||||
}
|
||||
|
||||
// Using both flags lets the OS loader decide how to load
|
||||
// it most efficiently. Either mode will prevent other
|
||||
// processes from modifying the module while it is loaded.
|
||||
IntPtr hModule = Kernel32.LoadLibraryEx(peFile, IntPtr.Zero,
|
||||
Kernel32.LoadLibraryFlags.LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE |
|
||||
Kernel32.LoadLibraryFlags.LOAD_LIBRARY_AS_IMAGE_RESOURCE);
|
||||
if (hModule == IntPtr.Zero)
|
||||
{
|
||||
ThrowExceptionForLastWin32Error();
|
||||
}
|
||||
|
||||
var enumTypesCallback = new Kernel32.EnumResTypeProc(EnumAndUpdateTypesCallback);
|
||||
var errorInfo = new EnumResourcesErrorInfo();
|
||||
GCHandle errorInfoHandle = GCHandle.Alloc(errorInfo);
|
||||
var errorInfoPtr = GCHandle.ToIntPtr(errorInfoHandle);
|
||||
|
||||
try
|
||||
{
|
||||
if (!Kernel32.EnumResourceTypes(hModule, enumTypesCallback, errorInfoPtr))
|
||||
{
|
||||
if (Marshal.GetHRForLastWin32Error() != Kernel32.ResourceDataNotFoundHRESULT)
|
||||
{
|
||||
CaptureEnumResourcesErrorInfo(errorInfoPtr);
|
||||
errorInfo.ThrowException();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
errorInfoHandle.Free();
|
||||
|
||||
if (!Kernel32.FreeLibrary(hModule))
|
||||
{
|
||||
ThrowExceptionForLastWin32Error();
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
internal static bool IsIntResource(IntPtr lpType)
|
||||
{
|
||||
return ((uint)lpType >> 16) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a language-neutral integer resource from a byte[] with
|
||||
/// a particular type and name. This will not modify the
|
||||
/// target until Update() is called.
|
||||
/// Throws an InvalidOperationException if Update() was already called.
|
||||
/// </summary>
|
||||
public ResourceUpdater AddResource(byte[] data, IntPtr lpType, IntPtr lpName)
|
||||
{
|
||||
if (hUpdate.IsInvalid)
|
||||
{
|
||||
ThrowExceptionForInvalidUpdate();
|
||||
}
|
||||
|
||||
if (!IsIntResource(lpType) || !IsIntResource(lpName))
|
||||
{
|
||||
throw new ArgumentException("AddResource can only be used with integer resource types");
|
||||
}
|
||||
|
||||
if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, Kernel32.LangID_LangNeutral_SublangNeutral, data, (uint)data.Length))
|
||||
{
|
||||
ThrowExceptionForLastWin32Error();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a language-neutral integer resource from a byte[] with
|
||||
/// a particular type and name. This will not modify the
|
||||
/// target until Update() is called.
|
||||
/// Throws an InvalidOperationException if Update() was already called.
|
||||
/// </summary>
|
||||
public ResourceUpdater AddResource(byte[] data, string lpType, IntPtr lpName)
|
||||
{
|
||||
if (hUpdate.IsInvalid)
|
||||
{
|
||||
ThrowExceptionForInvalidUpdate();
|
||||
}
|
||||
|
||||
if (!IsIntResource(lpName))
|
||||
{
|
||||
throw new ArgumentException("AddResource can only be used with integer resource names");
|
||||
}
|
||||
|
||||
if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, Kernel32.LangID_LangNeutral_SublangNeutral, data, (uint)data.Length))
|
||||
{
|
||||
ThrowExceptionForLastWin32Error();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the pending resource updates to the target PE
|
||||
/// file. After this, the ResourceUpdater no longer maintains
|
||||
/// an update handle, and can not be used for further updates.
|
||||
/// Throws an InvalidOperationException if Update() was already called.
|
||||
/// </summary>
|
||||
public void Update()
|
||||
{
|
||||
if (hUpdate.IsInvalid)
|
||||
{
|
||||
ThrowExceptionForInvalidUpdate();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!Kernel32.EndUpdateResource(hUpdate, false))
|
||||
{
|
||||
ThrowExceptionForLastWin32Error();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
hUpdate.SetHandleAsInvalid();
|
||||
}
|
||||
}
|
||||
|
||||
private bool EnumAndUpdateTypesCallback(IntPtr hModule, IntPtr lpType, IntPtr lParam)
|
||||
{
|
||||
var enumNamesCallback = new Kernel32.EnumResNameProc(EnumAndUpdateNamesCallback);
|
||||
if (!Kernel32.EnumResourceNames(hModule, lpType, enumNamesCallback, lParam))
|
||||
{
|
||||
CaptureEnumResourcesErrorInfo(lParam);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool EnumAndUpdateNamesCallback(IntPtr hModule, IntPtr lpType, IntPtr lpName, IntPtr lParam)
|
||||
{
|
||||
var enumLanguagesCallback = new Kernel32.EnumResLangProc(EnumAndUpdateLanguagesCallback);
|
||||
if (!Kernel32.EnumResourceLanguages(hModule, lpType, lpName, enumLanguagesCallback, lParam))
|
||||
{
|
||||
CaptureEnumResourcesErrorInfo(lParam);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool EnumAndUpdateLanguagesCallback(IntPtr hModule, IntPtr lpType, IntPtr lpName, ushort wLang, IntPtr lParam)
|
||||
{
|
||||
IntPtr hResource = Kernel32.FindResourceEx(hModule, lpType, lpName, wLang);
|
||||
if (hResource == IntPtr.Zero)
|
||||
{
|
||||
CaptureEnumResourcesErrorInfo(lParam);
|
||||
return false;
|
||||
}
|
||||
|
||||
// hResourceLoaded is just a handle to the resource, which
|
||||
// can be used to get the resource data
|
||||
IntPtr hResourceLoaded = Kernel32.LoadResource(hModule, hResource);
|
||||
if (hResourceLoaded == IntPtr.Zero)
|
||||
{
|
||||
CaptureEnumResourcesErrorInfo(lParam);
|
||||
return false;
|
||||
}
|
||||
|
||||
// This doesn't actually lock memory. It just retrieves a
|
||||
// pointer to the resource data. The pointer is valid
|
||||
// until the module is unloaded.
|
||||
IntPtr lpResourceData = Kernel32.LockResource(hResourceLoaded);
|
||||
if (lpResourceData == IntPtr.Zero)
|
||||
{
|
||||
((EnumResourcesErrorInfo)GCHandle.FromIntPtr(lParam).Target).failedToLockResource = true;
|
||||
}
|
||||
|
||||
if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, wLang, lpResourceData, Kernel32.SizeofResource(hModule, hResource)))
|
||||
{
|
||||
CaptureEnumResourcesErrorInfo(lParam);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private class EnumResourcesErrorInfo
|
||||
{
|
||||
public int hResult;
|
||||
public bool failedToLockResource;
|
||||
|
||||
public void ThrowException()
|
||||
{
|
||||
if (failedToLockResource)
|
||||
{
|
||||
Debug.Assert(hResult == 0);
|
||||
throw new ResourceNotAvailableException("Failed to lock resource");
|
||||
}
|
||||
|
||||
Debug.Assert(hResult != 0);
|
||||
throw new HResultException(hResult);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CaptureEnumResourcesErrorInfo(IntPtr errorInfoPtr)
|
||||
{
|
||||
int hResult = Marshal.GetHRForLastWin32Error();
|
||||
if (hResult != Kernel32.UserStoppedResourceEnumerationHRESULT)
|
||||
{
|
||||
GCHandle errorInfoHandle = GCHandle.FromIntPtr(errorInfoPtr);
|
||||
var errorInfo = (EnumResourcesErrorInfo)errorInfoHandle.Target;
|
||||
errorInfo.hResult = hResult;
|
||||
}
|
||||
}
|
||||
|
||||
private class ResourceNotAvailableException : Exception
|
||||
{
|
||||
public ResourceNotAvailableException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static void ThrowExceptionForLastWin32Error()
|
||||
{
|
||||
throw new HResultException(Marshal.GetHRForLastWin32Error());
|
||||
}
|
||||
|
||||
private static void ThrowExceptionForInvalidUpdate()
|
||||
{
|
||||
throw new InvalidOperationException("Update handle is invalid. This instance may not be used for further updates");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
hUpdate.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,4 @@
|
||||
<ProjectReference Include="..\Squirrel\Squirrel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="..\..\vendor\Microsoft.NET.HostModel.dll" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
BIN
vendor/Microsoft.NET.HostModel.dll
vendored
BIN
vendor/Microsoft.NET.HostModel.dll
vendored
Binary file not shown.
4
vendor/README.md
vendored
4
vendor/README.md
vendored
@@ -1,11 +1,11 @@
|
||||
# Vendor Binaries
|
||||
This folder contains pre-compiled binaries from a variety of sources. These should be updated periodically.
|
||||
|
||||
### Microsoft.NET.HostModel.dll
|
||||
<!-- ### Microsoft.NET.HostModel.dll
|
||||
- This is a .NET SDK 6.0.100 binary.
|
||||
- It's purpose is to allow us to re-pack the `Update.exe` single file bundle with a new exe icon.
|
||||
- Can be found in the dotnet SDK at "C:\Program Files\dotnet\sdk\6.0.100\Microsoft.NET.HostModel.dll".
|
||||
- MIT License: https://github.com/dotnet/runtime/blob/main/LICENSE.TXT
|
||||
- MIT License: https://github.com/dotnet/runtime/blob/main/LICENSE.TXT -->
|
||||
|
||||
### singlefilehost.exe
|
||||
- This is the native exe that has the .net native runtime linked in.
|
||||
|
||||
Reference in New Issue
Block a user