New setup architecture; more cross platform friendly and no need to load everything into memory

This commit is contained in:
Caelan Sayler
2022-02-19 18:33:11 +00:00
parent 061904dc23
commit 01aab4d763
45 changed files with 886 additions and 245 deletions

View File

@@ -24,6 +24,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SquirrelCli", "src\Squirrel
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Setup", "src\Setup\Setup.vcxproj", "{6B406985-B2E1-4FED-A405-BD0694D68E93}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squirrel.Shared", "src\Squirrel.Shared\Squirrel.Shared.csproj", "{352C15EA-622F-4132-80D8-9B6E3C83404E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
CIBuild|Any CPU = CIBuild|Any CPU
@@ -340,6 +342,54 @@ Global
{6B406985-B2E1-4FED-A405-BD0694D68E93}.Release|x64.Build.0 = Release|x64
{6B406985-B2E1-4FED-A405-BD0694D68E93}.Release|x86.ActiveCfg = Release|Win32
{6B406985-B2E1-4FED-A405-BD0694D68E93}.Release|x86.Build.0 = Release|Win32
{352C15EA-622F-4132-80D8-9B6E3C83404E}.CIBuild|Any CPU.ActiveCfg = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.CIBuild|Any CPU.Build.0 = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.CIBuild|Mixed Platforms.ActiveCfg = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.CIBuild|Mixed Platforms.Build.0 = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.CIBuild|x64.ActiveCfg = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.CIBuild|x64.Build.0 = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.CIBuild|x86.ActiveCfg = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.CIBuild|x86.Build.0 = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Coverage|Any CPU.ActiveCfg = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Coverage|Any CPU.Build.0 = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Coverage|Mixed Platforms.ActiveCfg = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Coverage|Mixed Platforms.Build.0 = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Coverage|x64.ActiveCfg = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Coverage|x64.Build.0 = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Coverage|x86.ActiveCfg = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Coverage|x86.Build.0 = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Debug|x64.ActiveCfg = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Debug|x64.Build.0 = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Debug|x86.ActiveCfg = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Debug|x86.Build.0 = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Mono Debug|Any CPU.ActiveCfg = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Mono Debug|Any CPU.Build.0 = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Mono Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Mono Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Mono Debug|x64.ActiveCfg = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Mono Debug|x64.Build.0 = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Mono Debug|x86.ActiveCfg = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Mono Debug|x86.Build.0 = Debug|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Mono Release|Any CPU.ActiveCfg = Release|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Mono Release|Any CPU.Build.0 = Release|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Mono Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Mono Release|Mixed Platforms.Build.0 = Release|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Mono Release|x64.ActiveCfg = Release|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Mono Release|x64.Build.0 = Release|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Mono Release|x86.ActiveCfg = Release|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Mono Release|x86.Build.0 = Release|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Release|Any CPU.Build.0 = Release|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Release|x64.ActiveCfg = Release|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Release|x64.Build.0 = Release|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Release|x86.ActiveCfg = Release|Any CPU
{352C15EA-622F-4132-80D8-9B6E3C83404E}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -2,54 +2,12 @@
#include <versionhelpers.h>
#include <string>
#include <functional>
#include <tchar.h>
#include "unzip.h"
#include "bundle_marker.h"
#include "platform_util.h"
using namespace std;
wstring getTempExePath()
{
wchar_t tempFolderBuf[MAX_PATH];
DWORD cTempFolder = GetTempPath(MAX_PATH, tempFolderBuf);
wchar_t tempFileBuf[MAX_PATH];
GetTempFileName(tempFolderBuf, L"squirrel", 0, tempFileBuf);
DeleteFile(tempFileBuf);
wstring tempFile(tempFileBuf);
tempFile += L".exe";
return tempFile;
}
BYTE* getByteResource(int idx, DWORD* cBuf)
{
auto f = FindResource(NULL, MAKEINTRESOURCE(idx), L"DATA");
if (!f) throw wstring(L"Unable to find resource " + to_wstring(idx));
auto r = LoadResource(NULL, f);
if (!r) throw wstring(L"Unable to load resource " + to_wstring(idx));
*cBuf = SizeofResource(NULL, f);
return (BYTE*)LockResource(r);
}
wstring getCurrentExecutablePath()
{
wchar_t ourFile[MAX_PATH];
HMODULE hMod = GetModuleHandle(NULL);
GetModuleFileName(hMod, ourFile, _countof(ourFile));
return wstring(ourFile);
}
wstring getNameFromPath(wstring path)
{
auto idx = path.find_last_of('\\');
// if we can't find last \ or the name is too short, default to 'Setup'
if (idx == wstring::npos || path.length() < idx + 3)
return L"Setup";
return path.substr(idx + 1);
}
// https://stackoverflow.com/a/874160/184746
bool hasEnding(std::wstring const& fullString, std::wstring const& ending)
{
@@ -90,96 +48,51 @@ void unzipSingleFile(BYTE* zipBuf, DWORD cZipBuf, wstring fileLocation, std::fun
if (!unzipSuccess) throw wstring(L"Unable to extract embedded package (predicate not found).");
}
// https://stackoverflow.com/a/17387176/184746
void throwLastWin32Error()
{
DWORD errorMessageID = ::GetLastError();
if (errorMessageID == 0) {
return;
}
LPWSTR messageBuffer = nullptr;
size_t size = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&messageBuffer, 0, NULL);
std::wstring message(messageBuffer, size);
throw message;
}
void wexec(const wchar_t* cmd)
{
LPTSTR szCmdline = _tcsdup(cmd); // https://stackoverflow.com/a/10044348/184746
STARTUPINFO si = { 0 };
si.cb = sizeof(STARTUPINFO);
si.wShowWindow = SW_SHOW;
si.dwFlags = STARTF_USESHOWWINDOW;
PROCESS_INFORMATION pi = { 0 };
if (!CreateProcess(NULL, szCmdline, NULL, NULL, false, 0, NULL, NULL, &si, &pi)) {
throwLastWin32Error();
}
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD dwExitCode = 0;
if (!GetExitCodeProcess(pi.hProcess, &dwExitCode)) {
dwExitCode = (DWORD)-9;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
if (dwExitCode != 0) {
throw wstring(L"Process exited with error code: " + to_wstring(dwExitCode));
}
}
int showErrorDialog(wstring msg)
{
wstring myPath = getCurrentExecutablePath();
wstring myName = getNameFromPath(myPath);
wstring errorTitle = myName + L" Error";
MessageBox(0, msg.c_str(), errorTitle.c_str(), MB_OK | MB_ICONERROR);
return -1;
}
int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ PWSTR pCmdLine, _In_ int nCmdShow)
{
if (!IsWindows7SP1OrGreater()) {
return showErrorDialog(L"This application requires Windows 7 SP1 or later and cannot be installed on this computer.");
util::show_error_dialog(L"This application requires Windows 7 SP1 or later and cannot be installed on this computer.");
return 0;
}
wstring myPath = getCurrentExecutablePath();
wstring updaterPath = getTempExePath();
wstring myPath = util::get_current_process_path();
wstring updaterPath = util::get_temp_file_path(L"exe");
try {
// locate bundled package
DWORD cZipBuf;
BYTE* zipBuf = getByteResource(205, &cZipBuf); // 205 = BundledPackageBytes
BYTE* memAddr = util::mmap_read(myPath, 0);
int64_t packageOffset, packageLength;
bundle_marker_t::header_offset(&packageOffset, &packageLength);
BYTE* pkgStart = memAddr + packageOffset;
if (packageOffset == 0 || packageLength == 0) {
util::munmap(memAddr);
util::show_error_dialog(L"An error occurred while running setup. The embedded package was not found. Please contact the application author.");
return 0;
}
// extract Squirrel installer
std::function<bool(ZIPENTRY&)> endsWithSquirrel([](ZIPENTRY& z) {
return hasEnding(std::wstring(z.name), L"Squirrel.exe");
});
unzipSingleFile(zipBuf, cZipBuf, updaterPath, endsWithSquirrel);
unzipSingleFile(pkgStart, packageLength, updaterPath, endsWithSquirrel);
util::munmap(memAddr);
// run installer and forward our command line arguments
wstring cmd = L"\"" + updaterPath + L"\" --setup \"" + myPath + L"\" " + pCmdLine;
wexec(cmd.c_str());
util::wexec(cmd.c_str());
}
catch (wstring wsx) {
return showErrorDialog(L"An error occurred while running setup: " + wsx + L". Please contact the application author.");
util::show_error_dialog(L"An error occurred while running setup. " + wsx + L". Please contact the application author.");
}
catch (std::exception ex) {
// nasty shit to convert from ascii to wide-char. this will fail if there are multi-byte characters.
// hopefully we remember to throw 'wstring' everywhere instead of 'exception' and it doesn't matter.
string msg = ex.what();
wstring wsTmp(msg.begin(), msg.end());
return showErrorDialog(L"An error occurred while running setup: " + wsTmp + L". Please contact the application author.");
util::show_error_dialog(L"An error occurred while running setup. " + wsTmp + L". Please contact the application author.");
}
catch (...) {
return showErrorDialog(L"An unknown error occurred while running setup. Please contact the application author.");
util::show_error_dialog(L"An unknown error occurred while running setup. Please contact the application author.");
}
// clean-up after ourselves

View File

@@ -87,10 +87,14 @@
</Manifest>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="bundle_marker.cpp" />
<ClCompile Include="platform_util.cpp" />
<ClCompile Include="Setup.cpp" />
<ClCompile Include="unzip.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="bundle_marker.h" />
<ClInclude Include="platform_util.h" />
<ClInclude Include="unzip.h" />
</ItemGroup>
<ItemGroup>

View File

@@ -21,11 +21,23 @@
<ClCompile Include="unzip.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="bundle_marker.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="platform_util.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="unzip.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="bundle_marker.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="platform_util.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Text Include="compatibility.manifest">

View File

@@ -0,0 +1,29 @@
#include "bundle_marker.h"
#include <windows.h>
#include <string>
using namespace std;
void bundle_marker_t::header_offset(int64_t* pOffset, int64_t* pLength)
{
// Contains the bundle_placeholder default value at compile time.
// the first 8 bytes are replaced by squirrel with the offset
// where the package is located.
static volatile uint8_t placeholder[] =
{
// 8 bytes represent the package offset
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 8 bytes represent the package length
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 64 bytes represent the bundle signature: SHA-256 for "squirrel bundle"
0x94, 0xf0, 0xb1, 0x7b, 0x68, 0x93, 0xe0, 0x29,
0x37, 0xeb, 0x34, 0xef, 0x53, 0xaa, 0xe7, 0xd4,
0x2b, 0x54, 0xf5, 0x70, 0x7e, 0xf5, 0xd6, 0xf5,
0x78, 0x54, 0x98, 0x3e, 0x5e, 0x94, 0xed, 0x7d
};
volatile bundle_marker_t* marker = reinterpret_cast<volatile bundle_marker_t*>(placeholder);
*pOffset = marker->locator.bundle_header_offset;
*pLength = marker->locator.bundle_header_length;
}

28
src/Setup/bundle_marker.h Normal file
View File

@@ -0,0 +1,28 @@
#ifndef __BUNDLE_MARKER_H__
#define __BUNDLE_MARKER_H__
#include <cstdint>
#pragma pack(push, 1)
union bundle_marker_t
{
public:
uint8_t placeholder[48];
struct
{
int64_t bundle_header_offset;
int64_t bundle_header_length;
uint8_t signature[32];
} locator;
static void header_offset(int64_t* pOffset, int64_t* pLength);
static bool is_bundle()
{
int64_t offset, length;
header_offset(&offset, &length);
return offset != 0;
}
};
#pragma pack(pop)
#endif // __BUNDLE_MARKER_H__

148
src/Setup/platform_util.cpp Normal file
View File

@@ -0,0 +1,148 @@
#include "platform_util.h"
#include <windows.h>
#include <tchar.h>
#include <string>
using namespace std;
wstring get_filename_from_path(wstring& path)
{
auto idx = path.find_last_of('\\');
// if we can't find last \ or the name is too short, default to 'Setup'
if (idx == wstring::npos || path.length() < idx + 3)
return L"Setup";
return path.substr(idx + 1);
}
// https://stackoverflow.com/a/17387176/184746
void throwLastWin32Error(wstring addedInfo)
{
DWORD errorMessageID = ::GetLastError();
if (errorMessageID == 0) {
return;
}
LPWSTR messageBuffer = nullptr;
size_t size = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&messageBuffer, 0, NULL);
wstring message(messageBuffer, size);
if (addedInfo.empty()) {
throw message;
}
else {
throw wstring(addedInfo + L" \n" + message);
}
}
std::wstring util::get_temp_file_path(wstring extension)
{
wchar_t tempFolderBuf[MAX_PATH];
DWORD cTempFolder = GetTempPath(MAX_PATH, tempFolderBuf);
wchar_t tempFileBuf[MAX_PATH];
GetTempFileName(tempFolderBuf, L"squirrel", 0, tempFileBuf);
DeleteFile(tempFileBuf);
wstring tempFile(tempFileBuf);
if (!extension.empty())
tempFile += L"." + extension;
return tempFile;
}
std::wstring util::get_current_process_path()
{
wchar_t ourFile[MAX_PATH];
HMODULE hMod = GetModuleHandle(NULL);
GetModuleFileName(hMod, ourFile, _countof(ourFile));
return wstring(ourFile);
}
void util::wexec(const wchar_t* cmd)
{
LPTSTR szCmdline = _tcsdup(cmd); // https://stackoverflow.com/a/10044348/184746
STARTUPINFO si = { 0 };
si.cb = sizeof(STARTUPINFO);
si.wShowWindow = SW_SHOW;
si.dwFlags = STARTF_USESHOWWINDOW;
PROCESS_INFORMATION pi = { 0 };
if (!CreateProcess(NULL, szCmdline, NULL, NULL, false, 0, NULL, NULL, &si, &pi)) {
throwLastWin32Error(L"Unable to start install process.");
}
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD dwExitCode = 0;
if (!GetExitCodeProcess(pi.hProcess, &dwExitCode)) {
dwExitCode = (DWORD)-9;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
if (dwExitCode != 0) {
throw wstring(L"Process exited with error code: " + to_wstring(dwExitCode));
}
}
void util::show_error_dialog(std::wstring msg)
{
wstring myPath = get_current_process_path();
wstring myName = get_filename_from_path(myPath);
wstring errorTitle = myName + L" Error";
MessageBox(0, msg.c_str(), errorTitle.c_str(), MB_OK | MB_ICONERROR);
}
// https://github.com/dotnet/runtime/blob/26c9b2883e0b6daaa98304fdc2912abec25dc216/src/native/corehost/hostmisc/pal.windows.cpp#L68
void* map_file_impl(const wstring& path, size_t* length, DWORD mapping_protect, DWORD view_desired_access)
{
HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (file == INVALID_HANDLE_VALUE) {
throwLastWin32Error(L"Failed to map file. CreateFileW() failed with error.");
}
if (length != nullptr) {
LARGE_INTEGER fileSize;
if (GetFileSizeEx(file, &fileSize) == 0) {
CloseHandle(file);
throwLastWin32Error(L"Failed to map file. GetFileSizeEx() failed with error.");
}
*length = (size_t)fileSize.QuadPart;
}
HANDLE map = CreateFileMappingW(file, NULL, mapping_protect, 0, 0, NULL);
if (map == NULL) {
CloseHandle(file);
throwLastWin32Error(L"Failed to map file. CreateFileMappingW() failed with error.");
}
void* address = MapViewOfFile(map, view_desired_access, 0, 0, 0);
// The file-handle (file) and mapping object handle (map) can be safely closed
// once the file is mapped. The OS keeps the file open if there is an open mapping into the file.
CloseHandle(map);
CloseHandle(file);
if (address == NULL) {
throwLastWin32Error(L"Failed to map file. MapViewOfFile() failed with error.");
}
return address;
}
uint8_t* util::mmap_read(const std::wstring& filePath, size_t* length)
{
return (uint8_t*)map_file_impl(filePath, length, PAGE_READONLY, FILE_MAP_READ);
}
bool util::munmap(uint8_t* addr)
{
return UnmapViewOfFile(addr) != 0;
}

16
src/Setup/platform_util.h Normal file
View File

@@ -0,0 +1,16 @@
#ifndef __PLATFORM_UTIL_H__
#define __PLATFORM_UTIL_H__
#include <string>
namespace util
{
std::wstring get_temp_file_path(std::wstring extension);
std::wstring get_current_process_path();
void wexec(const wchar_t* cmd);
void show_error_dialog(std::wstring msg);
uint8_t* mmap_read(const std::wstring& filePath, size_t* length);
bool munmap(uint8_t* addr);
}
#endif // __PLATFORM_UTIL_H__

View File

@@ -7,7 +7,7 @@ namespace Microsoft.NET.HostModel
#if NET5_0_OR_GREATER
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
#endif
internal partial class ResourceUpdater
public partial class ResourceUpdater
{
public ResourceUpdater(string peFile, bool bDeleteExistingResources)
{

View File

@@ -13,7 +13,7 @@ namespace Microsoft.NET.HostModel
/// in a PE image. It currently only works on Windows, because it
/// requires various kernel32 APIs.
/// </summary>
internal partial class ResourceUpdater : IDisposable
public partial class ResourceUpdater : IDisposable
{
private sealed class Kernel32
{

View File

@@ -0,0 +1,92 @@
using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Threading;
using Microsoft.NET.HostModel;
using Microsoft.NET.HostModel.AppHost;
namespace Squirrel.Shared
{
public static class SetupBundle
{
public static bool IsBundle(string setupPath, out long bundleOffset, out long bundleLength)
{
byte[] bundleSignature = {
// 64 bytes represent the bundle signature: SHA-256 for "squirrel bundle"
0x94, 0xf0, 0xb1, 0x7b, 0x68, 0x93, 0xe0, 0x29,
0x37, 0xeb, 0x34, 0xef, 0x53, 0xaa, 0xe7, 0xd4,
0x2b, 0x54, 0xf5, 0x70, 0x7e, 0xf5, 0xd6, 0xf5,
0x78, 0x54, 0x98, 0x3e, 0x5e, 0x94, 0xed, 0x7d
};
long offset = 0;
long length = 0;
void FindBundleHeader()
{
using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(setupPath, FileMode.Open, null, 0, MemoryMappedFileAccess.Read))
using (MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read)) {
int position = BinaryUtils.SearchInFile(accessor, bundleSignature);
if (position == -1) {
throw new PlaceHolderNotFoundInAppHostException(bundleSignature);
}
offset = accessor.ReadInt64(position - 16);
length = accessor.ReadInt64(position - 8);
}
}
Utility.Retry(FindBundleHeader);
bundleOffset = offset;
bundleLength = length;
return bundleOffset != 0 && bundleLength != 0;
}
public static void CreatePackageBundle(string setupPath, string packagePath)
{
long bundleOffset, bundleLength;
using (var pkgStream = File.OpenRead(packagePath))
using (var setupStream = File.Open(setupPath, FileMode.Append, FileAccess.Write)) {
bundleOffset = setupStream.Position;
bundleLength = pkgStream.Length;
pkgStream.CopyTo(setupStream);
}
byte[] placeholder = {
// 8 bytes represent the package offset
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 8 bytes represent the package length
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 64 bytes represent the bundle signature: SHA-256 for "squirrel bundle"
0x94, 0xf0, 0xb1, 0x7b, 0x68, 0x93, 0xe0, 0x29,
0x37, 0xeb, 0x34, 0xef, 0x53, 0xaa, 0xe7, 0xd4,
0x2b, 0x54, 0xf5, 0x70, 0x7e, 0xf5, 0xd6, 0xf5,
0x78, 0x54, 0x98, 0x3e, 0x5e, 0x94, 0xed, 0x7d
};
var data = new byte[16];
Array.Copy(BitConverter.GetBytes(bundleOffset), data, 8);
Array.Copy(BitConverter.GetBytes(bundleLength), 0, data, 8, 8);
// replace the beginning of the placeholder with the bytes from 'data'
RetryUtil.RetryOnIOError(() =>
BinaryUtils.SearchAndReplace(setupPath, placeholder, data, pad0s: false));
// memory-mapped write does not updating last write time
RetryUtil.RetryOnIOError(() =>
File.SetLastWriteTimeUtc(setupPath, DateTime.UtcNow));
if (!IsBundle(setupPath, out var offset, out var length))
throw new InvalidOperationException("Internal logic error writing setup bundle.");
}
public static Stream ReadPackageBundle(string setupPath)
{
if (!IsBundle(setupPath, out var offset, out var length))
throw new InvalidOperationException("The provided executable has no embedded Squirrel package.");
return new SubStream(File.OpenRead(setupPath), offset, length, new SemaphoreSlim(1), false);
}
}
}

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Squirrel\Squirrel.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,395 @@
// from https://raw.githubusercontent.com/Azure/azure-storage-net/master/Lib/Common/Blob/SubStream.cs
// Apache License 2.0
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Squirrel.Shared
{
/// <summary>
/// A wrapper class that creates a logical substream from a region within an existing seekable stream.
/// Allows for concurrent, asynchronous read and seek operations on the wrapped stream.
/// Ensures thread-safe operations between related substream instances via a shared, user-supplied synchronization mutex.
/// This class will buffer read requests to minimize overhead on the underlying stream.
/// </summary>
public sealed class SubStream : Stream
{
// Stream to be logically wrapped.
private Stream wrappedStream;
// Position in the wrapped stream at which the SubStream should logically begin.
private long streamBeginIndex;
private readonly bool leaveOpen;
// Total length of the substream.
private long substreamLength;
// Tracks the current position in the substream.
private long substreamCurrentIndex;
// Stream to manage read buffer, lazily initialized when read or seek operations commence.
private Lazy<MemoryStream> readBufferStream;
// Internal read buffer, lazily initialized when read or seek operations commence.
private Lazy<byte[]> readBuffer;
// Tracks the valid bytes remaining in the readBuffer
private int readBufferLength;
// Determines where to update the position of the readbuffer stream depending on the scenario)
private bool shouldSeek = false;
// Non-blocking semaphore for controlling read operations between related SubStream instances.
public SemaphoreSlim Mutex { get; set; }
public const int DefaultBufferSize = (int) (64 * 1024);
public const int DefaultSubStreamBufferSize = (int) (4 * 1024 * 1024);
// Current relative position in the substream.
public override long Position {
get {
return this.substreamCurrentIndex;
}
set {
AssertInBounds("Position", value, 0, this.substreamLength);
// Check if we can potentially advance substream position without reallocating the read buffer.
if (value >= this.substreamCurrentIndex) {
long offset = value - this.substreamCurrentIndex;
// New position is within the valid bytes stored in the readBuffer.
if (offset <= this.readBufferLength) {
this.readBufferLength -= (int) offset;
if (shouldSeek) {
this.readBufferStream.Value.Seek(offset, SeekOrigin.Current);
}
} else {
// Resets the read buffer.
this.readBufferLength = 0;
this.readBufferStream.Value.Seek(0, SeekOrigin.End);
}
} else {
// Resets the read buffer.
this.readBufferLength = 0;
this.readBufferStream.Value.Seek(0, SeekOrigin.End);
}
this.substreamCurrentIndex = value;
}
}
// Total length of the substream.
public override long Length {
get { return this.substreamLength; }
}
public override bool CanRead {
get { return true; }
}
public override bool CanSeek {
get { return true; }
}
public override bool CanWrite {
get { return false; }
}
private void CheckDisposed()
{
if (this.wrappedStream == null) {
throw new ObjectDisposedException("SubStreamWrapper");
}
}
protected override void Dispose(bool disposing)
{
if (!leaveOpen)
this.wrappedStream.Dispose();
this.wrappedStream = null;
this.readBufferStream = null;
this.readBuffer = null;
}
public override void Flush()
{
throw new NotSupportedException();
}
// Initiates the new buffer size to be used for read operations.
public int ReadBufferSize {
get {
return this.readBuffer.Value.Length;
}
set {
if (value < 2 * DefaultBufferSize) {
throw new ArgumentOutOfRangeException("ReadBufferSize", "ArgumentTooSmallError");
}
this.readBuffer = new Lazy<byte[]>(() => new byte[value]);
this.readBufferStream = new Lazy<MemoryStream>(() => new MemoryStream(this.readBuffer.Value, 0, value, true));
this.readBufferStream.Value.Seek(0, SeekOrigin.End);
}
}
/// <summary>
/// Creates a new SubStream instance.
/// </summary>
/// <param name="stream">A seekable source stream.</param>
/// <param name="streamBeginIndex">The index in the wrapped stream where the logical SubStream should begin.</param>
/// <param name="substreamLength">The length of the SubStream.</param>
/// <param name="globalSemaphore"> A <see cref="SemaphoreSlim"/> object that is shared between related SubStream instances.</param>
/// <param name="leaveOpen">Whether the underlying stream should be left open or not when this is disposed.</param>
/// <remarks>
/// The source stream to be wrapped must be seekable.
/// The Semaphore object provided must have the initialCount thread parameter set to one to ensure only one concurrent request is granted at a time.
/// </remarks>
public SubStream(Stream stream, long streamBeginIndex, long substreamLength, SemaphoreSlim globalSemaphore, bool leaveOpen)
{
if (stream == null) {
throw new ArgumentNullException("Stream.");
} else if (!stream.CanSeek) {
throw new NotSupportedException("Stream must be seekable.");
} else if (globalSemaphore == null) {
throw new ArgumentNullException("globalSemaphore");
}
AssertInBounds("streamBeginIndex", streamBeginIndex, 0, stream.Length);
this.streamBeginIndex = streamBeginIndex;
this.wrappedStream = stream;
this.Mutex = globalSemaphore;
this.leaveOpen = leaveOpen;
this.substreamLength = Math.Min(substreamLength, stream.Length - streamBeginIndex);
this.readBufferLength = 0;
this.Position = 0;
this.ReadBufferSize = DefaultSubStreamBufferSize;
}
#if !(WINDOWS_RT || NETCORE)
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
{
return AsApm(ReadAsync(buffer, offset, count, CancellationToken.None), callback, state);
}
public override int EndRead(IAsyncResult asyncResult)
{
AssertNotNull("AsyncResult", asyncResult);
return RunWithoutSynchronizationContext(() => ((Task<int>) asyncResult).Result);
}
#endif
/// <summary>
/// Reads a block of bytes asynchronously from the substream read buffer.
/// </summary>
/// <param name="buffer">When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source.</param>
/// <param name="offset">The zero-based byte offset in buffer at which to begin storing the data read from the current stream.</param>
/// <param name="count">The maximum number of bytes to be read.</param>
/// <param name="cancellationToken">An object of type <see cref="CancellationToken"/> that propagates notification that operation should be canceled.</param>
/// <returns>The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero if the end of the substream has been reached.</returns>
/// <remarks>
/// If the read request cannot be satisfied because the read buffer is empty or contains less than the requested number of the bytes,
/// the wrapped stream will be called to refill the read buffer.
/// Only one read request to the underlying wrapped stream will be allowed at a time and concurrent requests will be queued up by effect of the shared semaphore mutex.
/// </remarks>
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
this.CheckDisposed();
try {
int readCount = this.CheckAdjustReadCount(count, offset, buffer.Length);
int bytesRead = await this.readBufferStream.Value.ReadAsync(buffer, offset, Math.Min(readCount, this.readBufferLength), cancellationToken).ConfigureAwait(false);
int bytesLeft = readCount - bytesRead;
// must adjust readbufferLength
this.shouldSeek = false;
this.Position += bytesRead;
if (bytesLeft > 0 && readBufferLength == 0) {
this.readBufferStream.Value.Position = 0;
int bytesAdded =
await this.ReadAsyncHelper(this.readBuffer.Value, 0, this.readBuffer.Value.Length, cancellationToken).ConfigureAwait(false);
this.readBufferLength = bytesAdded;
if (bytesAdded > 0) {
bytesLeft = Math.Min(bytesAdded, bytesLeft);
int secondRead = await this.readBufferStream.Value.ReadAsync(buffer, bytesRead + offset, bytesLeft, cancellationToken).ConfigureAwait(false);
bytesRead += secondRead;
this.Position += secondRead;
}
}
return bytesRead;
} finally {
this.shouldSeek = true;
}
}
/// <summary>
/// Reads a block of bytes from the wrapped stream asynchronously and writes the data to the SubStream buffer.
/// </summary>
/// <param name="buffer">When this method returns, the substream read buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source.</param>
/// <param name="offset">The zero-based byte offset in buffer at which to begin storing the data read from the current stream.</param>
/// <param name="count">The maximum number of bytes to be read.</param>
/// <param name="cancellationToken">An object of type <see cref="CancellationToken"/> that propagates notification that operation should be canceled.</param>
/// <returns>The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero if the end of the substream has been reached.</returns>
/// <remarks>
/// This method will allow only one read request to the underlying wrapped stream at a time,
/// while concurrent requests will be queued up by effect of the shared semaphore mutex.
/// The caller is responsible for adjusting the substream position after a successful read.
/// </remarks>
private async Task<int> ReadAsyncHelper(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
await this.Mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
int result = 0;
try {
this.CheckDisposed();
// Check if read is out of range and adjust to read only up to the substream bounds.
count = this.CheckAdjustReadCount(count, offset, buffer.Length);
// Only seek if wrapped stream is misaligned with the substream position.
if (this.wrappedStream.Position != this.streamBeginIndex + this.Position) {
this.wrappedStream.Seek(this.streamBeginIndex + this.Position, SeekOrigin.Begin);
}
result = await this.wrappedStream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
} finally {
this.Mutex.Release();
}
return result;
}
public override int Read(byte[] buffer, int offset, int count)
{
return RunWithoutSynchronizationContext(() => this.ReadAsync(buffer, offset, count).Result);
}
/// <summary>
/// Sets the position within the current substream.
/// This operation does not perform a seek on the wrapped stream.
/// </summary>
/// <param name="offset">A byte offset relative to the origin parameter.</param>
/// <param name="origin">A value of type System.IO.SeekOrigin indicating the reference point used to obtain the new position.</param>
/// <returns>The new position within the current substream.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="offset"/> is invalid for SeekOrigin.</exception>
public override long Seek(long offset, SeekOrigin origin)
{
this.CheckDisposed();
long startIndex;
// Map offset to the specified SeekOrigin of the substream.
switch (origin) {
case SeekOrigin.Begin:
startIndex = 0;
break;
case SeekOrigin.Current:
startIndex = this.Position;
break;
case SeekOrigin.End:
startIndex = this.substreamLength;
break;
default:
throw new ArgumentOutOfRangeException();
}
this.Position = startIndex + offset;
return this.Position;
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
private int CheckAdjustReadCount(int count, int offset, int bufferLength)
{
if (offset < 0 || count < 0 || offset + count > bufferLength) {
throw new ArgumentOutOfRangeException();
}
long currentPos = this.streamBeginIndex + this.Position;
long endPos = this.streamBeginIndex + this.substreamLength;
if (currentPos + count > endPos) {
return (int) (endPos - currentPos);
} else {
return count;
}
}
internal static void AssertInBounds<T>(string paramName, T val, T min, T max)
where T : IComparable
{
if (val.CompareTo(min) < 0) {
throw new ArgumentOutOfRangeException(paramName, "ArgumentTooSmallError");
}
if (val.CompareTo(max) > 0) {
throw new ArgumentOutOfRangeException(paramName, "ArgumentTooLargeError");
}
}
internal static void AssertNotNull(string paramName, object value)
{
if (value == null) {
throw new ArgumentNullException(paramName);
}
}
internal static void RunWithoutSynchronizationContext(Action actionToRun)
{
SynchronizationContext oldContext = SynchronizationContext.Current;
try {
SynchronizationContext.SetSynchronizationContext(null);
actionToRun();
} finally {
SynchronizationContext.SetSynchronizationContext(oldContext);
}
}
internal static T RunWithoutSynchronizationContext<T>(Func<T> actionToRun)
{
SynchronizationContext oldContext = SynchronizationContext.Current;
try {
SynchronizationContext.SetSynchronizationContext(null);
return actionToRun();
} finally {
SynchronizationContext.SetSynchronizationContext(oldContext);
}
}
// Copied from https://msdn.microsoft.com/en-us/library/hh873178.aspx
internal static IAsyncResult AsApm<T>(Task<T> task, AsyncCallback callback, object state)
{
if (task == null)
throw new ArgumentNullException("task");
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>(state);
task.ContinueWith(t => {
if (t.IsFaulted)
tcs.TrySetException(t.Exception.InnerExceptions);
else if (t.IsCanceled)
tcs.TrySetCanceled();
else
tcs.TrySetResult(t.Result);
if (callback != null)
callback(tcs.Task);
}, TaskScheduler.Default);
return tcs.Task;
}
}
}

View File

@@ -1,76 +0,0 @@
using System;
using System.Text;
using Microsoft.NET.HostModel;
namespace Squirrel.Lib
{
#if NET5_0_OR_GREATER
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
#endif
internal class BundledSetupInfo
{
public string AppId { get; set; }
public string AppFriendlyName { get; set; } = "This Application";
public string[] RequiredFrameworks { get; set; } = new string[0];
public string BundledPackageName { get; set; }
public byte[] BundledPackageBytes { get; set; }
public byte[] SplashImageBytes { get; set; }
public byte[] SetupIconBytes { get; set; }
private const string RESOURCE_TYPE = "DATA";
private static readonly ushort RESOURCE_LANG = 0x0409;
public static BundledSetupInfo ReadFromFile(string exePath)
{
var bundle = new BundledSetupInfo();
using var reader = new ResourceReader(exePath);
bundle.AppId = ReadString(reader, 200);
bundle.AppFriendlyName = ReadString(reader, 201);
bundle.SplashImageBytes = ReadBytes(reader, 202);
bundle.RequiredFrameworks = ReadString(reader, 203)?.Split(',') ?? new string[0];
bundle.BundledPackageName = ReadString(reader, 204);
bundle.BundledPackageBytes = ReadBytes(reader, 205);
bundle.SetupIconBytes = ReadBytes(reader, 206);
return bundle;
}
private static byte[] ReadBytes(ResourceReader reader, int idx)
{
return reader.ReadResource(RESOURCE_TYPE, new IntPtr(idx), RESOURCE_LANG);
}
private static string ReadString(ResourceReader reader, int idx)
{
var bytes = ReadBytes(reader, idx);
if (bytes == null) return null;
return Encoding.Unicode.GetString(bytes).TrimEnd('\0');
}
public void WriteToFile(string exePath)
{
using var writer = new ResourceUpdater(exePath);
WriteValue(writer, 200, AppId);
WriteValue(writer, 201, AppFriendlyName);
WriteValue(writer, 202, SplashImageBytes);
WriteValue(writer, 203, String.Join(",", RequiredFrameworks ?? new string[0]));
WriteValue(writer, 204, BundledPackageName);
WriteValue(writer, 205, BundledPackageBytes);
WriteValue(writer, 206, SetupIconBytes);
writer.Update();
}
private void WriteValue(ResourceUpdater updater, int idx, string str)
{
if (String.IsNullOrWhiteSpace(str)) return;
var bytes = Encoding.Unicode.GetBytes(String.Concat(str, "\0\0"));
WriteValue(updater, idx, bytes);
}
private void WriteValue(ResourceUpdater updater, int idx, byte[] buf)
{
if (buf == null || buf.Length == 0) return;
updater.AddResource(buf, RESOURCE_TYPE, new IntPtr(idx), RESOURCE_LANG);
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
@@ -36,12 +36,7 @@ namespace Squirrel
public string InputPackageFile { get; protected set; }
public string ReleasePackageFile { get; protected set; }
public string SuggestedReleaseFileName {
get {
var zp = new ZipPackage(InputPackageFile);
return String.Format("{0}-{1}-full.nupkg", zp.Id, zp.Version);
}
}
public string SuggestedReleaseFileName => new ZipPackage(InputPackageFile).FullReleaseFilename;
public SemanticVersion Version => ReleaseEntry.ParseEntryFileName(InputPackageFile).Version;

View File

@@ -36,6 +36,7 @@ namespace Squirrel.NuGet
public string ProductDescription => Description ?? Summary ?? Title ?? Id;
public string ProductCompany => (Authors.Any() ? String.Join(", ", Authors) : Owners) ?? ProductName;
public string ProductCopyright => Copyright ?? "Copyright © " + DateTime.Now.Year.ToString() + " " + ProductCompany;
public string FullReleaseFilename => String.Format("{0}-{1}-full.nupkg", Id, Version);
public string Id { get; private set; }
public SemanticVersion Version { get; private set; }
@@ -51,6 +52,10 @@ namespace Squirrel.NuGet
public IEnumerable<ZipPackageFile> Files { get; private set; } = Enumerable.Empty<ZipPackageFile>();
public RuntimeCpu MachineArchitecture { get; private set; }
public byte[] SetupSplashBytes { get; private set; }
public byte[] SetupIconBytes { get; private set; }
public byte[] AppIconBytes { get; private set; }
protected string Description { get; private set; }
protected IEnumerable<string> Authors { get; private set; } = Enumerable.Empty<string>();
protected string Owners { get; private set; }
@@ -70,6 +75,27 @@ namespace Squirrel.NuGet
ReadManifest(manifest);
Files = GetPackageFiles(zip).ToArray();
Frameworks = GetFrameworks(Files);
// we pre-load some images so the zip doesn't need to be opened again later
SetupSplashBytes = ReadFileToBytes(zip, z => Path.GetFileNameWithoutExtension(z.Key) == "splashimage");
SetupIconBytes = ReadFileToBytes(zip, z => z.Key == "setup.ico");
AppIconBytes = ReadFileToBytes(zip, z => z.Key == "app.ico") ?? ReadFileToBytes(zip, z => z.Key.EndsWith("app.ico"));
}
private byte[] ReadFileToBytes(ZipArchive archive, Func<ZipArchiveEntry, bool> predicate)
{
var f = archive.Entries.FirstOrDefault(predicate);
if (f == null)
return null;
using var stream = f.OpenEntryStream();
if (stream == null)
return null;
var ms = new MemoryStream();
stream.CopyTo(ms);
return ms.ToArray();
}
public static void SetSquirrelMetadata(string nuspecPath, RuntimeCpu architecture, IEnumerable<string> runtimes)

View File

@@ -8,16 +8,18 @@ namespace Squirrel.NuGet
{
internal interface IPackageFile : IFrameworkTargetable
{
Uri Key { get; }
string Path { get; }
string EffectivePath { get; }
string TargetFramework { get; }
bool IsLibFile();
bool IsContentFile();
Stream GetEntryStream(Stream archiveStream);
//Stream GetEntryStream(Stream archiveStream);
}
internal class ZipPackageFile : IPackageFile, IEquatable<ZipPackageFile>
{
public Uri Key { get; }
public string EffectivePath { get; }
public string TargetFramework { get; }
public string Path { get; }
@@ -31,22 +33,21 @@ namespace Squirrel.NuGet
}
}
private readonly Uri _entryKey;
public ZipPackageFile(Uri relpath)
{
_entryKey = relpath;
Key = relpath;
Path = NugetUtil.GetPath(relpath);
TargetFramework = NugetUtil.ParseFrameworkNameFromFilePath(Path, out var effectivePath);
EffectivePath = effectivePath;
}
public Stream GetEntryStream(Stream archiveStream)
{
using var zip = ZipArchive.Open(archiveStream, new() { LeaveStreamOpen = true });
var entry = zip.Entries.FirstOrDefault(f => new Uri(f.Key, UriKind.Relative) == _entryKey);
return entry?.OpenEntryStream();
}
//public Stream GetEntryStream(Stream archiveStream)
//{
// using var zip = ZipArchive.Open(archiveStream, new() { LeaveStreamOpen = true });
// var entry = zip.Entries.FirstOrDefault(f => new Uri(f.Key, UriKind.Relative) == _entryKey);
// return entry?.OpenEntryStream();
//}
public bool IsLibFile() => IsFileInTopDirectory(NugetUtil.LibDirectory);
public bool IsContentFile() => IsFileInTopDirectory(NugetUtil.ContentDirectory);

View File

@@ -5,6 +5,7 @@ using System.Runtime.InteropServices;
[assembly: InternalsVisibleTo("Squirrel.Tests, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Update, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("SquirrelCli, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Squirrel.Shared, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Squirrel, PublicKey=" + SNK.SHA1)]
internal static class SNK

View File

@@ -24,7 +24,7 @@ namespace Squirrel
}
const string uninstallRegSubKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall";
public async Task<RegistryKey> CreateUninstallerRegistryEntry(string uninstallCmd, string quietSwitch)
public Task<RegistryKey> CreateUninstallerRegistryEntry(string uninstallCmd, string quietSwitch)
{
this.Log().Info($"Writing uninstaller registry entry");
var releaseContent = File.ReadAllText(Path.Combine(rootAppDirectory, "packages", "RELEASES"), Encoding.UTF8);
@@ -45,14 +45,9 @@ namespace Squirrel
// we will try to find an "app.ico" from the package, write it to the local app dir, and then
// use it for the uninstaller icon. If an app.ico does not exist, it will use a SquirrelAwareApp exe icon instead.
try {
var appIconEntry = zp.Files
.FirstOrDefault(f => f.IsLibFile() && f.EffectivePath.Equals("app.ico", StringComparison.InvariantCultureIgnoreCase));
if (appIconEntry != null) {
if (zp.AppIconBytes != null) {
var targetIco = Path.Combine(rootAppDirectory, "app.ico");
using (var pkgStream = File.OpenRead(pkgPath))
using (var iconStream = appIconEntry.GetEntryStream(pkgStream))
using (var targetStream = File.Open(targetIco, FileMode.Create, FileAccess.Write))
await iconStream.CopyToAsync(targetStream).ConfigureAwait(false);
File.WriteAllBytes(targetIco, zp.AppIconBytes);
this.Log().Info($"File '{targetIco}' is being used for uninstall icon.");
key.SetValue("DisplayIcon", targetIco, RegistryValueKind.String);
} else {
@@ -101,7 +96,7 @@ namespace Squirrel
key.SetValue(kvp.Key, kvp.Value, RegistryValueKind.DWord);
}
return key;
return Task.FromResult(key);
}
public void KillAllProcessesBelongingToPackage()

View File

@@ -93,9 +93,8 @@ namespace SquirrelCli
public string baseUrl { get; private set; }
public string framework { get; private set; }
public string splashImage { get; private set; }
public string updateIcon { get; private set; }
public string icon { get; private set; }
public string appIcon { get; private set; }
public string setupIcon { get; private set; }
public bool noDelta { get; private set; }
public bool allowUnaware { get; private set; }
public string msi { get; private set; }
@@ -113,8 +112,7 @@ namespace SquirrelCli
Add("noDelta", "Skip the generation of delta packages", v => noDelta = true);
Add("f=|framework=", "List of required {RUNTIMES} to install during setup\nexample: 'net6,vcredist143'", v => framework = v);
Add("s=|splashImage=", "{PATH} to image/gif displayed during installation", v => splashImage = v);
Add("i=|icon=", "{PATH} to .ico for Setup.exe and Update.exe",
(v) => { updateIcon = v; setupIcon = v; });
Add("i=|icon=", "{PATH} to .ico for Setup.exe and Update.exe", v => icon = v);
Add("appIcon=", "{PATH} to .ico for 'Apps and Features' list", v => appIcon = v);
Add("msi=", "Compile a .msi machine-wide deployment tool with the specified {BITNESS}. (either 'x86' or 'x64')", v => msi = v.ToLower());
}
@@ -127,8 +125,7 @@ namespace SquirrelCli
protected virtual void ValidateInternal(bool checkPackage)
{
IsValidFile(nameof(appIcon), ".ico");
IsValidFile(nameof(setupIcon), ".ico");
IsValidFile(nameof(updateIcon), ".ico");
IsValidFile(nameof(icon), ".ico");
IsValidFile(nameof(splashImage));
IsValidUrl(nameof(baseUrl));

View File

@@ -18,6 +18,7 @@ using Squirrel;
using Squirrel.Json;
using Squirrel.Lib;
using Squirrel.NuGet;
using Squirrel.Shared;
using Squirrel.SimpleSplat;
using SquirrelCli.Sources;
@@ -138,7 +139,7 @@ namespace SquirrelCli
var baseUrl = options.baseUrl;
var generateDeltas = !options.noDelta;
var backgroundGif = options.splashImage;
var setupIcon = options.setupIcon;
var setupIcon = options.icon ?? options.appIcon;
if (!package.EndsWith(".nupkg", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException("package must be packed with nuget and end in '.nupkg'");
@@ -153,8 +154,8 @@ namespace SquirrelCli
// update icon for Update.exe if requested
var bundledUpdatePath = HelperExe.UpdatePath(p => Microsoft.NET.HostModel.AppHost.HostWriter.IsBundle(p, out var _hz));
var updatePath = Path.Combine(tempDir, "Update.exe");
if (options.updateIcon != null) {
DotnetUtil.UpdateSingleFileBundleIcon(bundledUpdatePath, updatePath, options.updateIcon).Wait();
if (setupIcon != null) {
DotnetUtil.UpdateSingleFileBundleIcon(bundledUpdatePath, updatePath, setupIcon).Wait();
} else {
File.Copy(bundledUpdatePath, updatePath, true);
}
@@ -275,6 +276,10 @@ namespace SquirrelCli
ico.Save(fs);
}
}
// copy other images to root (used by setup)
if (setupIcon != null) File.Copy(setupIcon, Path.Combine(pkgPath, "setup.ico"), true);
if (backgroundGif != null) File.Copy(backgroundGif, Path.Combine(pkgPath, "splashimage" + Path.GetExtension(backgroundGif)));
});
processed.Add(rp.ReleasePackageFile);
@@ -310,18 +315,7 @@ namespace SquirrelCli
var newestReleasePath = Path.Combine(di.FullName, newestFullRelease.Filename);
Log.Info($"Creating Setup bundle");
var infosave = new BundledSetupInfo() {
AppId = bundledzp.Id,
AppFriendlyName = bundledzp.ProductName,
BundledPackageBytes = File.ReadAllBytes(newestReleasePath),
BundledPackageName = Path.GetFileName(newestReleasePath),
RequiredFrameworks = requiredFrameworks.Select(r => r.Id).ToArray(),
};
if (setupIcon != null) infosave.SetupIconBytes = File.ReadAllBytes(setupIcon);
if (backgroundGif != null) infosave.SplashImageBytes = File.ReadAllBytes(backgroundGif);
infosave.WriteToFile(targetSetupExe);
SetupBundle.CreatePackageBundle(targetSetupExe, newestReleasePath);
options.SignPEFile(targetSetupExe);
if (!String.IsNullOrEmpty(options.msi)) {

View File

@@ -32,7 +32,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squirrel\Squirrel.csproj" />
<ProjectReference Include="..\Squirrel.Shared\Squirrel.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -120,7 +120,10 @@ namespace Squirrel.Update
static async Task Setup(string setupPath, bool silentInstall, bool checkInstall)
{
Log.Info($"Extracting bundled app data from '{setupPath}'.");
var info = BundledSetupInfo.ReadFromFile(setupPath);
using var pkgStream = Shared.SetupBundle.ReadPackageBundle(setupPath);
var zp = new ZipPackage(pkgStream, true);
var appname = zp.ProductName;
if (checkInstall) {
// CS: migrated from MachineInstaller.cpp
@@ -136,14 +139,14 @@ namespace Squirrel.Update
// bailing out
// C:\Users\Username\AppData\Local\$pkgName
var localadinstall = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), info.AppId);
var localadinstall = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), zp.Id);
if (Directory.Exists(localadinstall)) {
Log.Info($"App install detected at '{localadinstall}', exiting...");
return;
}
// C:\ProgramData\$pkgName\$username
var programdatainstall = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), info.AppId, Environment.UserName);
var programdatainstall = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), zp.Id, Environment.UserName);
if (Directory.Exists(programdatainstall)) {
Log.Info($"App install detected at '{programdatainstall}', exiting...");
return;
@@ -154,16 +157,15 @@ namespace Squirrel.Update
}
using var _t = Utility.WithTempDirectory(out var tempFolder);
ISplashWindow splash = new Windows.User32SplashWindow(info.AppFriendlyName, silentInstall, info.SetupIconBytes, info.SplashImageBytes);
ISplashWindow splash = new Windows.User32SplashWindow(appname, silentInstall, zp.SetupIconBytes, zp.SetupSplashBytes);
// verify that this package can be installed on this cpu architecture
var zp = new ZipPackage(new MemoryStream(info.BundledPackageBytes));
if (AssemblyRuntimeInfo.Architecture == RuntimeCpu.X86 && zp.MachineArchitecture == RuntimeCpu.X64) {
splash.ShowErrorDialog("Incompatible System", "The current operating system uses the x86 cpu architecture, but this package requires an x64 system.");
return;
}
var missingFrameworks = info.RequiredFrameworks
var missingFrameworks = zp.RuntimeDependencies
.Select(f => Runtimes.GetRuntimeByName(f))
.Where(f => f != null)
.Where(f => !f.CheckIsInstalled().Result)
@@ -172,9 +174,9 @@ namespace Squirrel.Update
// prompt user to install missing dependencies
if (missingFrameworks.Any()) {
string message = missingFrameworks.Length > 1
? $"{info.AppFriendlyName} is missing the following system components: {String.Join(", ", missingFrameworks.Select(s => s.DisplayName))}. " +
? $"{appname} is missing the following system components: {String.Join(", ", missingFrameworks.Select(s => s.DisplayName))}. " +
$"Would you like to install these now?"
: $"{info.AppFriendlyName} requires {missingFrameworks.First().DisplayName} installed to continue, would you like to install it now?";
: $"{appname} requires {missingFrameworks.First().DisplayName} installed to continue, would you like to install it now?";
if (!splash.ShowQuestionDialog("Missing System Components", message)) {
return; // user cancelled install
@@ -215,11 +217,16 @@ namespace Squirrel.Update
}
}
// setup package source directory
Log.Info($"Starting package install from directory " + tempFolder);
splash.SetProgressIndeterminate();
string packagePath = Path.Combine(tempFolder, info.BundledPackageName);
File.WriteAllBytes(packagePath, info.BundledPackageBytes);
// copy package to directory
string packagePath = Path.Combine(tempFolder, zp.FullReleaseFilename);
pkgStream.Position = 0;
using (var writeStream = File.Open(packagePath, FileMode.Create, FileAccess.ReadWrite))
pkgStream.CopyTo(writeStream);
// create RELEASES file for UpdateManager to read
var entry = ReleaseEntry.GenerateFromFile(packagePath);
ReleaseEntry.WriteReleaseFile(new[] { entry }, Path.Combine(tempFolder, "RELEASES"));

View File

@@ -19,7 +19,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Squirrel\Squirrel.csproj" />
<ProjectReference Include="..\Squirrel.Shared\Squirrel.Shared.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -60,6 +60,8 @@ namespace Squirrel.Update.Windows
_signal = new ManualResetEvent(false);
try {
// we only accept a byte array and convert to memorystream because
// gdi needs to seek and get length which is not supported in DeflateStream
if (iconBytes?.Length > 0) _icon = new Icon(new MemoryStream(iconBytes));
if (splashBytes?.Length > 0) _img = (Bitmap) Bitmap.FromStream(new MemoryStream(splashBytes));
} catch (Exception ex) {