winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 9ad1353d976013d4108b98a2e31cb33e68981b1c
parent 384789e3619b96b740eb0e55c101d9cda0e7b4bb
Author: JohnMcPMS <johnmcp@microsoft.com>
Date:   Mon,  2 Mar 2020 15:34:08 -0800

Implement Microsoft.PreIndexed.Package source update and remove (#45)


Diffstat:
Msrc/AppInstallerCLI/AppInstallerCLI.vcxproj | 4++--
Msrc/AppInstallerCLICore/Commands/SourceCommand.cpp | 27++++++++++++++++-----------
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 9++++-----
Msrc/AppInstallerCLICore/Workflows/WorkflowReporter.h | 1+
Msrc/AppInstallerCLIPackage/Package.appxmanifest | 1+
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 24++++++++++++++++--------
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 12++++++++++++
Msrc/AppInstallerCLITests/Downloader.cpp | 4++--
Asrc/AppInstallerCLITests/MsixInfo.cpp | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLITests/PreIndexedPackageSource.cpp | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/Sources.cpp | 69++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/AppInstallerCLITests/TestCommon.cpp | 33++++++++++++++++++++++++++++++++-
Msrc/AppInstallerCLITests/TestCommon.h | 25+++++++++++++++++++++++--
Asrc/AppInstallerCLITests/TestData/index.1.0.0.0.msix | 0
Asrc/AppInstallerCLITests/TestData/index.2.0.0.0.msix | 0
Msrc/AppInstallerCommonCore/Deployment.cpp | 10+++++++++-
Msrc/AppInstallerCommonCore/Downloader.cpp | 19+++++++++++++++++--
Msrc/AppInstallerCommonCore/MsixInfo.cpp | 224++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/AppInstallerCommonCore/Public/AppInstallerDeployment.h | 7++++++-
Msrc/AppInstallerCommonCore/Public/AppInstallerDownloader.h | 7+++++--
Msrc/AppInstallerCommonCore/Public/AppInstallerErrors.h | 1+
Msrc/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h | 26+++++++++++++++++++++++++-
Msrc/AppInstallerCommonCore/Public/AppInstallerProgress.h | 43+++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Public/AppInstallerRuntime.h | 3+++
Msrc/AppInstallerCommonCore/Runtime.cpp | 72+++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/AppInstallerCommonCore/pch.h | 2++
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj | 20++++++++++----------
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters | 8++++----
Asrc/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp | 259+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.h | 31+++++++++++++++++++++++++++++++
Dsrc/AppInstallerRepositoryCore/Microsoft/PreIndexedSource.cpp | 48------------------------------------------------
Dsrc/AppInstallerRepositoryCore/Microsoft/PreIndexedSource.h | 39---------------------------------------
Msrc/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h | 11++++++-----
Msrc/AppInstallerRepositoryCore/RepositorySource.cpp | 67+++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/AppInstallerRepositoryCore/SourceFactory.h | 10+++++++---
Msrc/AppInstallerRepositoryCore/pch.h | 5+++++
36 files changed, 1151 insertions(+), 215 deletions(-)

diff --git a/src/AppInstallerCLI/AppInstallerCLI.vcxproj b/src/AppInstallerCLI/AppInstallerCLI.vcxproj @@ -133,7 +133,7 @@ <Link> <SubSystem>Console</SubSystem> <GenerateWindowsMetadata>false</GenerateWindowsMetadata> - <AdditionalDependencies Condition="'$(Configuration)'=='Debug'">wininet.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)'=='Debug'">wininet.lib;shell32.lib;shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies> </Link> </ItemDefinitionGroup> <ItemDefinitionGroup Condition="'$(Platform)'=='Win32'"> @@ -159,7 +159,7 @@ <EnableCOMDATFolding>true</EnableCOMDATFolding> <OptimizeReferences>true</OptimizeReferences> <GenerateWindowsMetadata>false</GenerateWindowsMetadata> - <AdditionalDependencies Condition="'$(Configuration)'=='Release'">wininet.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)'=='Release'">wininet.lib;shell32.lib;shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies> </Link> </ItemDefinitionGroup> <ItemGroup> diff --git a/src/AppInstallerCLICore/Commands/SourceCommand.cpp b/src/AppInstallerCLICore/Commands/SourceCommand.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "SourceCommand.h" #include "Localization.h" +#include "Workflows/WorkflowReporter.h" namespace AppInstaller::CLI @@ -61,7 +62,7 @@ namespace AppInstaller::CLI }; } - void SourceAddCommand::ExecuteInternal(Invocation& inv, std::ostream& out, std::istream&) const + void SourceAddCommand::ExecuteInternal(Invocation& inv, std::ostream& out, std::istream& in) const { std::string name = *inv.GetArg(s_SourceCommand_ArgName_Name); std::string arg = *inv.GetArg(s_SourceCommand_ArgName_Arg); @@ -79,8 +80,8 @@ namespace AppInstaller::CLI out << " " << LOCME("Type: ") << type << std::endl; } - // TODO: Needs to be hooked up to a reporter when real source construction happens. - Repository::AddSource(std::move(name), std::move(type), std::move(arg)); + Workflow::WorkflowReporter reporter(out, in); + reporter.ExecuteWithProgress(std::bind(Repository::AddSource, std::move(name), std::move(type), std::move(arg), std::placeholders::_1)); out << LOCME("Done") << std::endl; } @@ -170,15 +171,17 @@ namespace AppInstaller::CLI }; } - void SourceUpdateCommand::ExecuteInternal(Invocation& inv, std::ostream& out, std::istream&) const + void SourceUpdateCommand::ExecuteInternal(Invocation& inv, std::ostream& out, std::istream& in) const { + Workflow::WorkflowReporter reporter(out, in); + if (inv.Contains(s_SourceCommand_ArgName_Name)) { const std::string& name = *inv.GetArg(s_SourceCommand_ArgName_Name); out << LOCME("Updating source: ") << name << "..." << std::endl; - if (!Repository::UpdateSource(name)) + if (!reporter.ExecuteWithProgress(std::bind(Repository::UpdateSource, name, std::placeholders::_1))) { - out << LOCME("Could not find a source by that name.") << std::endl; + out << std::endl << LOCME(" Could not find a source by that name.") << std::endl; } else { @@ -192,9 +195,9 @@ namespace AppInstaller::CLI std::vector<Repository::SourceDetails> sources = Repository::GetSources(); for (const auto& sd : sources) { - out << LOCME(" Updating source: ") << sd.Name << "..." << std::flush; - Repository::UpdateSource(sd.Name); - out << LOCME(" Done.") << std::endl; + out << LOCME("Updating source: ") << sd.Name << "..." << std::endl; + reporter.ExecuteWithProgress(std::bind(Repository::UpdateSource, sd.Name, std::placeholders::_1)); + out << LOCME("Done.") << std::endl; } } } @@ -218,11 +221,13 @@ namespace AppInstaller::CLI }; } - void SourceRemoveCommand::ExecuteInternal(Invocation& inv, std::ostream& out, std::istream&) const + void SourceRemoveCommand::ExecuteInternal(Invocation& inv, std::ostream& out, std::istream& in) const { + Workflow::WorkflowReporter reporter(out, in); + const std::string& name = *inv.GetArg(s_SourceCommand_ArgName_Name); out << LOCME("Removing source: ") << name << "..." << std::endl; - if (!Repository::RemoveSource(name)) + if (!reporter.ExecuteWithProgress(std::bind(Repository::RemoveSource, name, std::placeholders::_1))) { out << LOCME("Could not find a source by that name.") << std::endl; } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -13,14 +13,13 @@ namespace AppInstaller::Workflow { void WorkflowBase::OpenIndexSource() { + std::string sourceName; if (m_argsRef.Contains(CLI::ARG_SOURCE)) { - m_source = OpenSource(*m_argsRef.GetArg(CLI::ARG_SOURCE)); - } - else - { - m_source = OpenSource(); + sourceName = *m_argsRef.GetArg(CLI::ARG_SOURCE); } + + m_source = m_reporter.ExecuteWithProgress(std::bind(OpenSource, sourceName, std::placeholders::_1)); } void WorkflowBase::IndexSearch() diff --git a/src/AppInstallerCLICore/Workflows/WorkflowReporter.h b/src/AppInstallerCLICore/Workflows/WorkflowReporter.h @@ -75,6 +75,7 @@ namespace AppInstaller::Workflow // IProgressCallback void OnProgress(uint64_t current, uint64_t maximum, ProgressType type) override; bool IsCancelled() override { return false; } + [[nodiscard]] IProgressCallback::CancelFunctionRemoval SetCancellationFunction(std::function<void()>&&) override { return {}; } // Runs the given callable of type: auto(IProgressCallback&) template <typename F> diff --git a/src/AppInstallerCLIPackage/Package.appxmanifest b/src/AppInstallerCLIPackage/Package.appxmanifest @@ -36,5 +36,6 @@ </Applications> <Capabilities> <rescap:Capability Name="runFullTrust" /> + <rescap:Capability Name="packageManagement" /> </Capabilities> </Package> \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -108,23 +108,23 @@ <ClCompile> <Optimization>Disabled</Optimization> <PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\YamlCppLib\yaml-cpp\include;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\YamlCppLib\yaml-cpp\include;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</TreatWarningAsError> </ClCompile> <Link> <SubSystem>Console</SubSystem> <GenerateWindowsMetadata>false</GenerateWindowsMetadata> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">wininet.lib;shell32.lib;winsqlite3.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies> </Link> </ItemDefinitionGroup> <ItemDefinitionGroup Condition="'$(Platform)'=='Win32'"> <ClCompile> <PreprocessorDefinitions>WIN32;%(PreprocessorDefinitions)</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\YamlCppLib\yaml-cpp\include;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\YamlCppLib\yaml-cpp\include;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</TreatWarningAsError> </ClCompile> <Link> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">wininet.lib;shell32.lib;winsqlite3.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies> </Link> </ItemDefinitionGroup> <ItemDefinitionGroup Condition="'$(Configuration)'=='Release'"> @@ -133,16 +133,16 @@ <FunctionLevelLinking>true</FunctionLevelLinking> <IntrinsicFunctions>true</IntrinsicFunctions> <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\YamlCppLib\yaml-cpp\include;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\YamlCppLib\yaml-cpp\include;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\YamlCppLib\yaml-cpp\include;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\YamlCppLib\yaml-cpp\include;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> </ClCompile> <Link> <SubSystem>Console</SubSystem> <EnableCOMDATFolding>true</EnableCOMDATFolding> <OptimizeReferences>true</OptimizeReferences> <GenerateWindowsMetadata>false</GenerateWindowsMetadata> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">wininet.lib;shell32.lib;winsqlite3.lib;%(AdditionalDependencies)</AdditionalDependencies> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|x64'">wininet.lib;shell32.lib;winsqlite3.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|x64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies> </Link> </ItemDefinitionGroup> <ItemGroup> @@ -152,6 +152,8 @@ </ItemGroup> <ItemGroup> <ClCompile Include="Downloader.cpp" /> + <ClCompile Include="MsixInfo.cpp" /> + <ClCompile Include="PreIndexedPackageSource.cpp" /> <ClCompile Include="WorkFlow.cpp" /> <ClCompile Include="LanguageUtilities.cpp" /> <ClCompile Include="main.cpp"> @@ -204,6 +206,12 @@ <CopyFileToFolders Include="TestData\InstallerArgTest_Msi_WithSwitches.yml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\index.1.0.0.0.msix"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\index.2.0.0.0.msix"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> </ItemGroup> <ItemGroup> <ProjectReference Include="..\AppInstallerCLICore\AppInstallerCLICore.vcxproj"> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -65,6 +65,12 @@ <ClCompile Include="Synchronization.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="MsixInfo.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="PreIndexedPackageSource.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> @@ -101,5 +107,11 @@ <CopyFileToFolders Include="TestData\InstallerArgTest_Msi_WithSwitches.yml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\index.1.0.0.0.msix"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\index.2.0.0.0.msix"> + <Filter>TestData</Filter> + </CopyFileToFolders> </ItemGroup> </Project> \ No newline at end of file diff --git a/src/AppInstallerCLITests/Downloader.cpp b/src/AppInstallerCLITests/Downloader.cpp @@ -50,12 +50,12 @@ TEST_CASE("DownloadValidFileAndCancel", "[Downloader]") REQUIRE(!waitResult.has_value()); } -TEST_CASE("DownloadUnreachableUrl", "[Downloader]") +TEST_CASE("DownloadInvalidUrl", "[Downloader]") { TestCommon::TempFile tempFile("downloader_test"s, ".test"s); INFO("Using temporary file named: " << tempFile.GetPath()); ProgressCallback callback; - REQUIRE_THROWS_HR(Download("https://does_not_exist.com/", tempFile.GetPath(), callback, true), WININET_E_NAME_NOT_RESOLVED); + REQUIRE_THROWS_HR(Download("blargle-flargle-fluff", tempFile.GetPath(), callback, true), WININET_E_UNRECOGNIZED_SCHEME); } diff --git a/src/AppInstallerCLITests/MsixInfo.cpp b/src/AppInstallerCLITests/MsixInfo.cpp @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include <AppInstallerMsixInfo.h> + +using namespace std::string_literals; +using namespace std::string_view_literals; +using namespace TestCommon; +using namespace AppInstaller; + +constexpr std::string_view s_MsixFile_1 = "index.1.0.0.0.msix"; +constexpr std::string_view s_MsixFile_2 = "index.2.0.0.0.msix"; + +TEST_CASE("MsixInfo_GetPackageFamilyName", "[msixinfo]") +{ + TestDataFile index(s_MsixFile_1); + Msix::MsixInfo msix(index.GetPath().u8string()); + + std::string expectedFamilyName = "AppInstallerCLITestsFakeIndex_125rzkzqaqjwj"; + std::string actualFamilyName = msix.GetPackageFamilyName(); + + REQUIRE(expectedFamilyName == actualFamilyName); +} + +TEST_CASE("MsixInfo_WriteManifestAndCompareToSelf", "[msixinfo]") +{ + TestDataFile index(s_MsixFile_1); + Msix::MsixInfo msix(index.GetPath().u8string()); + + TempFile manifest{ "msixtest_manifest"s, ".xml"s }; + ProgressCallback callback; + + msix.WriteManifestToFile(manifest, callback); + + REQUIRE(!msix.IsNewerThan(manifest)); +} + +TEST_CASE("MsixInfo_WriteManifestAndCompareToOlder", "[msixinfo]") +{ + TestDataFile index1(s_MsixFile_1); + Msix::MsixInfo msix1(index1.GetPath().u8string()); + + TempFile manifest{ "msixtest_manifest"s, ".xml"s }; + ProgressCallback callback; + + msix1.WriteManifestToFile(manifest, callback); + + TestDataFile index2(s_MsixFile_2); + Msix::MsixInfo msix2(index2.GetPath().u8string()); + + REQUIRE(msix2.IsNewerThan(manifest)); +} + +TEST_CASE("MsixInfo_WriteFile", "[msixinfo]") +{ + TestDataFile index(s_MsixFile_1); + Msix::MsixInfo msix(index.GetPath().u8string()); + + TempFile file{ "msixtest_file"s, ".bin"s }; + ProgressCallback callback; + + msix.WriteToFile("index.db", file, callback); + + REQUIRE(1 == std::filesystem::file_size(file)); +} diff --git a/src/AppInstallerCLITests/PreIndexedPackageSource.cpp b/src/AppInstallerCLITests/PreIndexedPackageSource.cpp @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include <AppInstallerRepositorySource.h> +#include <AppInstallerRuntime.h> +#include <AppInstallerStrings.h> +#include <Microsoft/PreIndexedPackageSourceFactory.h> + +using namespace std::string_literals; +using namespace std::string_view_literals; +using namespace TestCommon; +using namespace AppInstaller; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Runtime; +using namespace AppInstaller::Utility; + +namespace fs = std::filesystem; + +constexpr std::string_view s_RepositorySettings_UserSources = "usersources"sv; + +constexpr std::string_view s_MsixFile_1 = "index.1.0.0.0.msix"; +constexpr std::string_view s_MsixFile_2 = "index.2.0.0.0.msix"; +constexpr std::string_view s_Msix_FamilyName = "AppInstallerCLITestsFakeIndex_125rzkzqaqjwj"; +constexpr std::string_view s_AppxManifestFileName = "AppxManifest.xml"sv; +constexpr std::string_view s_IndexMsixName = "index.msix"sv; +constexpr std::string_view s_IndexFileName = "index.db"sv; + +void CopyIndexFileToDirectory(const fs::path& from, const fs::path& to) +{ + fs::path toFile = to; + toFile /= s_IndexMsixName; + if (fs::exists(toFile)) + { + fs::remove(toFile); + } + fs::copy_file(from, toFile); +} + +fs::path GetPathToFileDir() +{ + fs::path result = GetPathToLocalState(); + result /= AppInstaller::Repository::Microsoft::PreIndexedPackageSourceFactory::Type(); + result /= s_Msix_FamilyName; + return result; +} + +std::string GetContents(const fs::path& file) +{ + REQUIRE(fs::exists(file)); + std::ifstream stream(file); + return ReadEntireStream(stream); +} + +TEST_CASE("PIPS_Add", "[pips]") +{ + RemoveSetting(s_RepositorySettings_UserSources); + + TempDirectory dir("pipssource"); + TestDataFile index(s_MsixFile_1); + CopyIndexFileToDirectory(index, dir); + + std::string name = "TestName"; + std::string type(AppInstaller::Repository::Microsoft::PreIndexedPackageSourceFactory::Type()); + std::string arg = dir; + ProgressCallback callback; + + AddSource(name, type, arg, callback); + + fs::path state = GetPathToFileDir(); + REQUIRE(fs::exists(state)); + + fs::path manifest = state; + manifest /= s_AppxManifestFileName; + REQUIRE(fs::exists(manifest)); + REQUIRE(fs::file_size(manifest) > 0); + + fs::path indexFile = state; + indexFile /= s_IndexFileName; + REQUIRE(fs::exists(indexFile)); + REQUIRE(fs::file_size(indexFile) > 0); +} + +TEST_CASE("PIPS_UpdateSameVersion", "[pips]") +{ + RemoveSetting(s_RepositorySettings_UserSources); + + TempDirectory dir("pipssource"); + TestDataFile index(s_MsixFile_1); + CopyIndexFileToDirectory(index, dir); + + std::string name = "TestName"; + std::string type(AppInstaller::Repository::Microsoft::PreIndexedPackageSourceFactory::Type()); + std::string arg = dir; + TestProgress callback; + + AddSource(name, type, arg, callback); + + fs::path state = GetPathToFileDir(); + REQUIRE(fs::exists(state)); + + bool progressCalled = false; + callback.m_OnProgress = [&](uint64_t, uint64_t, ProgressType) { progressCalled = true; }; + + UpdateSource(name, callback); + REQUIRE(!progressCalled); +} + +TEST_CASE("PIPS_UpdateNewVersion", "[pips]") +{ + RemoveSetting(s_RepositorySettings_UserSources); + + TempDirectory dir("pipssource"); + TestDataFile indexMsix1(s_MsixFile_1); + CopyIndexFileToDirectory(indexMsix1, dir); + + std::string name = "TestName"; + std::string type(AppInstaller::Repository::Microsoft::PreIndexedPackageSourceFactory::Type()); + std::string arg = dir; + TestProgress callback; + + AddSource(name, type, arg, callback); + + fs::path state = GetPathToFileDir(); + REQUIRE(fs::exists(state)); + + fs::path manifestPath = state; + manifestPath /= s_AppxManifestFileName; + std::string manifestContents1 = GetContents(manifestPath); + + fs::path indexPath = state; + indexPath /= s_IndexFileName; + std::string indexContents1 = GetContents(indexPath); + + TestDataFile indexMsix2(s_MsixFile_2); + CopyIndexFileToDirectory(indexMsix2, dir); + + bool progressCalled = false; + callback.m_OnProgress = [&](uint64_t, uint64_t, ProgressType) { progressCalled = true; }; + + UpdateSource(name, callback); + REQUIRE(progressCalled); + + std::string manifestContents2 = GetContents(manifestPath); + REQUIRE(manifestContents1 != manifestContents2); + + std::string indexContents2 = GetContents(indexPath); + REQUIRE(indexContents1 != indexContents2); +} + +TEST_CASE("PIPS_Remove", "[pips]") +{ + RemoveSetting(s_RepositorySettings_UserSources); + + TempDirectory dir("pipssource"); + TestDataFile index(s_MsixFile_1); + CopyIndexFileToDirectory(index, dir); + + std::string name = "TestName"; + std::string type(AppInstaller::Repository::Microsoft::PreIndexedPackageSourceFactory::Type()); + std::string arg = dir; + ProgressCallback callback; + + AddSource(name, type, arg, callback); + + fs::path state = GetPathToFileDir(); + REQUIRE(fs::exists(state)); + + fs::path manifest = state; + manifest /= s_AppxManifestFileName; + REQUIRE(fs::exists(manifest)); + + fs::path indexFile = state; + indexFile /= s_IndexFileName; + REQUIRE(fs::exists(indexFile)); + + RemoveSource(name, callback); + REQUIRE(!fs::exists(state)); +} diff --git a/src/AppInstallerCLITests/Sources.cpp b/src/AppInstallerCLITests/Sources.cpp @@ -4,11 +4,12 @@ #include "TestCommon.h" #include "TestHooks.h" -#include <Public/AppInstallerRepositorySource.h> +#include <AppInstallerRepositorySource.h> #include <AppInstallerDateTime.h> #include <AppInstallerRuntime.h> #include <AppInstallerStrings.h> +using namespace AppInstaller; using namespace AppInstaller::Runtime; using namespace AppInstaller::Repository; using namespace AppInstaller::Utility; @@ -94,25 +95,31 @@ struct TestSource : public ISource // Helper that allows some lambdas to be wrapped into a source factory. struct TestSourceFactory : public ISourceFactory { + using IsInitializedFunctor = std::function<bool(const SourceDetails&)>; using CreateFunctor = std::function<std::unique_ptr<ISource>(const SourceDetails&)>; using UpdateFunctor = std::function<void(SourceDetails&)>; using RemoveFunctor = std::function<void(const SourceDetails&)>; TestSourceFactory() : - m_Create(TestSource::Create), m_Update([](SourceDetails&) {}), m_Remove([](const SourceDetails&) {}) {} + m_isInit([](const SourceDetails&) { return true; }), m_Create(TestSource::Create), m_Update([](SourceDetails&) {}), m_Remove([](const SourceDetails&) {}) {} // ISourceFactory + bool IsInitialized(const SourceDetails& details) override + { + return m_isInit(details); + } + std::unique_ptr<ISource> Create(const SourceDetails& details) override { return m_Create(details); } - void Update(SourceDetails& details) override + void Update(SourceDetails& details, IProgressCallback&) override { m_Update(details); } - void Remove(const SourceDetails& details) override + void Remove(const SourceDetails& details, IProgressCallback&) override { m_Remove(details); } @@ -123,6 +130,7 @@ struct TestSourceFactory : public ISourceFactory return [this]() { return std::make_unique<TestSourceFactory>(*this); }; } + IsInitializedFunctor m_isInit; CreateFunctor m_Create; UpdateFunctor m_Update; RemoveFunctor m_Remove; @@ -208,7 +216,8 @@ TEST_CASE("RepoSources_AddSource", "[sources]") factory.m_Update = [&](SourceDetails& sd) { updateCalledOnFactory = true; sd.Data = data; }; TestHook_SetSourceFactoryOverride(type, factory); - AddSource(name, type, arg); + ProgressCallback progress; + AddSource(name, type, arg, progress); REQUIRE(updateCalledOnFactory); @@ -237,7 +246,8 @@ TEST_CASE("RepoSources_AddMultipleSources", "[sources]") factory1.m_Update = [&](SourceDetails& sd) { sd.Data = data; }; TestHook_SetSourceFactoryOverride(type, factory1); - AddSource(name, type, arg); + ProgressCallback progress; + AddSource(name, type, arg, progress); std::vector<SourceDetails> sources = GetSources(); REQUIRE(sources.size() == 1); @@ -252,7 +262,7 @@ TEST_CASE("RepoSources_AddMultipleSources", "[sources]") factory2.m_Update = [&](SourceDetails& sd) { sd.Data = data + suffix[1]; }; TestHook_SetSourceFactoryOverride(type + suffix[1], factory2); - AddSource(name + suffix[1], type + suffix[1], arg + suffix[1]); + AddSource(name + suffix[1], type + suffix[1], arg + suffix[1], progress); sources = GetSources(); REQUIRE(sources.size() == 2); @@ -285,7 +295,8 @@ TEST_CASE("RepoSources_UpdateSource", "[sources]") factory.m_Update = [&](SourceDetails& sd) { updateCalledOnFactory = true; sd.Data = data; }; TestHook_SetSourceFactoryOverride(type, factory); - AddSource(name, type, arg); + ProgressCallback progress; + AddSource(name, type, arg, progress); REQUIRE(updateCalledOnFactory); @@ -303,7 +314,7 @@ TEST_CASE("RepoSources_UpdateSource", "[sources]") auto now = std::chrono::system_clock::now(); factory.m_Update = [&](SourceDetails& sd) { updateCalledOnFactory = true; sd.LastUpdateTime = now; }; - UpdateSource(name); + UpdateSource(name, progress); REQUIRE(updateCalledOnFactory); @@ -332,15 +343,51 @@ TEST_CASE("RepoSources_RemoveSource", "[sources]") factory.m_Remove = [&](const SourceDetails&) { removeCalledOnFactory = true; }; TestHook_SetSourceFactoryOverride(type, factory); - AddSource(name, type, arg); + ProgressCallback progress; + AddSource(name, type, arg, progress); std::vector<SourceDetails> sources = GetSources(); REQUIRE(sources.size() == 1); - RemoveSource(name); + RemoveSource(name, progress); REQUIRE(removeCalledOnFactory); sources = GetSources(); REQUIRE(sources.empty()); } + +TEST_CASE("RepoSources_UpdateOnOpen", "[sources]") +{ + using namespace std::chrono_literals; + + RemoveSetting(s_RepositorySettings_UserSources); + TestHook_ClearSourceFactoryOverrides(); + + std::string name = "testName"; + std::string type = "testType"; + std::string arg = "testArg"; + std::string data = "testDataOnUpdate"; + + bool updateCalledOnFactory = false; + TestSourceFactory factory; + factory.m_isInit = [](const SourceDetails&) { return false; }; + factory.m_Update = [&](SourceDetails& sd) { updateCalledOnFactory = true; sd.Data = data; }; + TestHook_SetSourceFactoryOverride(type, factory); + + SetSetting(s_RepositorySettings_UserSources, s_SingleSource); + + ProgressCallback progress; + auto source = OpenSource(name, progress); + + REQUIRE(updateCalledOnFactory); + + std::vector<SourceDetails> sources = GetSources(); + REQUIRE(sources.size() == 1); + + REQUIRE(sources[0].Name == name); + REQUIRE(sources[0].Type == type); + REQUIRE(sources[0].Arg == arg); + REQUIRE(sources[0].Data == data); + REQUIRE(sources[0].LastUpdateTime == ConvertUnixEpochToSystemClock(0)); +} diff --git a/src/AppInstallerCLITests/TestCommon.cpp b/src/AppInstallerCLITests/TestCommon.cpp @@ -65,7 +65,7 @@ namespace TestCommon switch (s_TempFileDestructorBehavior) { case TempFileDestructionBehavior::Delete: - std::filesystem::remove(_filepath); + std::filesystem::remove_all(_filepath); break; case TempFileDestructionBehavior::Keep: break; @@ -100,6 +100,19 @@ namespace TestCommon } } + TempDirectory::TempDirectory(const std::string& baseName, bool create) + { + _filepath = GetTempFilePath(baseName, ""); + if (create) + { + if (std::filesystem::exists(_filepath)) + { + std::filesystem::remove_all(_filepath); + } + std::filesystem::create_directories(_filepath); + } + } + std::filesystem::path TestDataFile::GetPath() const { std::filesystem::path result = s_TestDataFileBasePath; @@ -111,4 +124,22 @@ namespace TestCommon { s_TestDataFileBasePath = path; } + + void TestProgress::OnProgress(uint64_t current, uint64_t maximum, AppInstaller::ProgressType type) + { + if (m_OnProgress) + { + m_OnProgress(current, maximum, type); + } + } + + bool TestProgress::IsCancelled() + { + return false; + } + + AppInstaller::IProgressCallback::CancelFunctionRemoval TestProgress::SetCancellationFunction(std::function<void()>&&) + { + return {}; + } } diff --git a/src/AppInstallerCLITests/TestCommon.h b/src/AppInstallerCLITests/TestCommon.h @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include <AppInstallerProgress.h> #include <wil/result.h> #include <filesystem> +#include <functional> #include <string> #define SQLITE_MEMORY_DB_CONNECTION_TARGET ":memory:" @@ -34,16 +36,24 @@ namespace TestCommon ~TempFile(); const std::filesystem::path& GetPath() const { return _filepath; } - operator const std::string () const { return _filepath.u8string(); } + operator const std::filesystem::path& () const { return _filepath; } + operator const std::string() const { return _filepath.u8string(); } static void SetDestructorBehavior(TempFileDestructionBehavior behavior); static void SetTestFailed(bool failed); - private: + protected: + TempFile() = default; std::filesystem::path _filepath; }; + // Use to create a temporary directory for testing. + struct TempDirectory : public TempFile + { + TempDirectory(const std::string& baseName, bool create = true); + }; + // Use this to find a test data file when testing. struct TestDataFile { @@ -78,4 +88,15 @@ namespace TestCommon private: HRESULT m_expectedHR = S_OK; }; + + // An IProgressCallback that is easily hooked. + struct TestProgress : public AppInstaller::IProgressCallback + { + // Inherited via IProgressCallback + void OnProgress(uint64_t current, uint64_t maximum, AppInstaller::ProgressType type) override; + bool IsCancelled() override; + CancelFunctionRemoval SetCancellationFunction(std::function<void()>&& f) override; + + std::function<void(uint64_t, uint64_t, AppInstaller::ProgressType)> m_OnProgress; + }; } diff --git a/src/AppInstallerCLITests/TestData/index.1.0.0.0.msix b/src/AppInstallerCLITests/TestData/index.1.0.0.0.msix Binary files differ. diff --git a/src/AppInstallerCLITests/TestData/index.2.0.0.0.msix b/src/AppInstallerCLITests/TestData/index.2.0.0.0.msix Binary files differ. diff --git a/src/AppInstallerCommonCore/Deployment.cpp b/src/AppInstallerCommonCore/Deployment.cpp @@ -49,7 +49,8 @@ namespace AppInstaller::Deployment // Set progress callback. deployOperation.Progress(progressCallback); - auto deployResult = deployOperation.GetResults(); + auto removeCancel = callback.SetCancellationFunction([&]() { deployOperation.Cancel(); }); + auto deployResult = deployOperation.get(); if (!SUCCEEDED(deployResult.ExtendedErrorCode())) { @@ -62,4 +63,11 @@ namespace AppInstaller::Deployment AICLI_LOG(Core, Info, << "Successfully deployed #" << id); } } + + void RemovePackageFireAndForget(winrt::hstring packageFullName) + { + using namespace winrt::Windows::Management::Deployment; + PackageManager packageManager; + (void)packageManager.RemovePackageAsync(packageFullName, RemovalOptions::None); + } } diff --git a/src/AppInstallerCommonCore/Downloader.cpp b/src/AppInstallerCommonCore/Downloader.cpp @@ -123,4 +123,20 @@ namespace AppInstaller::Utility return result; } -}- \ No newline at end of file + + bool IsUrlRemote(std::string_view url) + { + using namespace std::string_view_literals; + constexpr std::string_view s_http_start = "http://"sv; + constexpr std::string_view s_https_start = "https://"sv; + + // Very simple choice right now: "does it start with http:// or https://"? + if (CaseInsensitiveEquals(url.substr(0, s_http_start.length()), s_http_start) || + CaseInsensitiveEquals(url.substr(0, s_https_start.length()), s_https_start)) + { + return true; + } + + return false; + } +} diff --git a/src/AppInstallerCommonCore/MsixInfo.cpp b/src/AppInstallerCommonCore/MsixInfo.cpp @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - #include "pch.h" +#include "Public/AppInstallerMsixInfo.h" #include "HttpStream/HttpRandomAccessStream.h" #include "Public/AppInstallerStrings.h" -#include "Public/AppInstallerMsixInfo.h" +#include "Public/AppInstallerDownloader.h" using namespace winrt::Windows::Storage::Streams; @@ -13,9 +13,103 @@ using namespace AppInstaller::Utility::HttpStream; namespace AppInstaller::Msix { + namespace + { + // Gets the version from the manifest reader. + UINT64 GetVersionFromManifestReader(IAppxManifestReader* reader) + { + ComPtr<IAppxManifestPackageId> packageId; + THROW_IF_FAILED(reader->GetPackageId(&packageId)); + + UINT64 result = 0; + THROW_IF_FAILED(packageId->GetVersion(&result)); + + return result; + } + + // Writes the stream (from current location) to the given file. + void WriteStreamToFile(IStream* stream, UINT64 expectedSize, const std::filesystem::path& target, IProgressCallback& progress) + { + std::filesystem::path tempFile = target; + tempFile += ".dnld"; + + { + std::ofstream file(tempFile, std::ios_base::binary | std::ios_base::out | std::ios_base::trunc); + + constexpr ULONG bufferSize = 1 << 20; + std::unique_ptr<char[]> buffer = std::make_unique<char[]>(bufferSize); + + UINT64 totalBytesRead = 0; + + while (!progress.IsCancelled()) + { + ULONG bytesRead = 0; + HRESULT hr = stream->Read(buffer.get(), bufferSize, &bytesRead); + + if (bytesRead) + { + // If we got bytes, just accept them and keep going. + LOG_IF_FAILED(hr); + + file.write(buffer.get(), bytesRead); + totalBytesRead += bytesRead; + progress.OnProgress(totalBytesRead, expectedSize, ProgressType::Bytes); + } + else + { + // If given a size, and we have read it all, quit + if (expectedSize && totalBytesRead == expectedSize) + { + break; + } + + // If the stream returned an error, throw it + THROW_IF_FAILED(hr); + + // If we were given a size and didn't reach it, throw our own error; + // otherwise assume that this is just normal EOF. + if (expectedSize) + { + THROW_WIN32(ERROR_HANDLE_EOF); + } + else + { + break; + } + } + } + } + + std::filesystem::path backupFile = target; + backupFile += ".bkup"; + if (std::filesystem::exists(target)) + { + if (std::filesystem::exists(backupFile)) + { + std::filesystem::remove(backupFile); + } + std::filesystem::rename(target, backupFile); + } + + std::filesystem::rename(tempFile, target); + } + + // Writes the appx file to the given file. + void WriteAppxFileToFile(IAppxFile* appxFile, const std::filesystem::path& target, IProgressCallback& progress) + { + UINT64 size = 0; + THROW_IF_FAILED(appxFile->GetSize(&size)); + + ComPtr<IStream> stream; + THROW_IF_FAILED(appxFile->GetStream(&stream)); + + WriteStreamToFile(stream.Get(), size, target, progress); + } + } + bool GetBundleReader( - _In_ IStream* inputStream, - _Outptr_ IAppxBundleReader** reader) + IStream* inputStream, + IAppxBundleReader** reader) { ComPtr<IAppxBundleFactory> bundleFactory; @@ -46,10 +140,9 @@ namespace AppInstaller::Msix } bool GetPackageReader( - _In_ IStream* inputStream, - _Outptr_ IAppxPackageReader** reader) + IStream* inputStream, + IAppxPackageReader** reader) { - ComPtr<IAppxFactory> appxFactory; // Create a new Appx factory @@ -79,16 +172,41 @@ namespace AppInstaller::Msix } } - MsixInfo::MsixInfo(const std::string& uriStr) + void GetManifestReader( + IStream* inputStream, + IAppxManifestReader** reader) { - // Get an IStream from the input uri and try to create package or bundler reader. - winrt::Windows::Foundation::Uri uri(Utility::ConvertToUTF16(uriStr)); - IRandomAccessStream randomAccessStream = HttpRandomAccessStream::CreateAsync(uri).get(); + ComPtr<IAppxFactory> appxFactory; - ::IUnknown* rasAsIUnknown = (::IUnknown*)winrt::get_abi(randomAccessStream); - THROW_IF_FAILED(CreateStreamOverRandomAccessStream( - rasAsIUnknown, - IID_PPV_ARGS(m_stream.ReleaseAndGetAddressOf()))); + THROW_IF_FAILED(CoCreateInstance( + __uuidof(AppxFactory), + nullptr, + CLSCTX_INPROC_SERVER, + __uuidof(IAppxFactory), + (LPVOID*)(&appxFactory))); + + THROW_IF_FAILED(appxFactory->CreateManifestReader(inputStream, reader)); + } + + MsixInfo::MsixInfo(std::string_view uriStr) + { + if (Utility::IsUrlRemote(uriStr)) + { + // Get an IStream from the input uri and try to create package or bundler reader. + winrt::Windows::Foundation::Uri uri(Utility::ConvertToUTF16(uriStr)); + IRandomAccessStream randomAccessStream = HttpRandomAccessStream::CreateAsync(uri).get(); + + ::IUnknown* rasAsIUnknown = (::IUnknown*)winrt::get_abi(randomAccessStream); + THROW_IF_FAILED(CreateStreamOverRandomAccessStream( + rasAsIUnknown, + IID_PPV_ARGS(m_stream.ReleaseAndGetAddressOf()))); + } + else + { + std::filesystem::path path(uriStr); + THROW_IF_FAILED(SHCreateStreamOnFileEx(path.c_str(), + STGM_READ | STGM_SHARE_DENY_WRITE | STGM_FAILIFTHERE, 0, FALSE, nullptr, &m_stream)); + } if (GetBundleReader(m_stream.Get(), &m_bundleReader)) { @@ -101,7 +219,7 @@ namespace AppInstaller::Msix else { THROW_HR_MSG(HRESULT_FROM_WIN32(ERROR_INSTALL_OPEN_PACKAGE_FAILED), - "Failed to open uri as msix package or bundle. Uri: %s", uriStr.c_str()); + "Failed to open uri as msix package or bundle. Uri: %s", uriStr.data()); } } @@ -136,4 +254,75 @@ namespace AppInstaller::Msix return signatureContent; } -}- \ No newline at end of file + + std::string MsixInfo::GetPackageFamilyName() + { + ComPtr<IAppxManifestPackageId> packageId; + if (m_isBundle) + { + ComPtr<IAppxBundleManifestReader> manifestReader; + THROW_IF_FAILED(m_bundleReader->GetManifest(&manifestReader)); + THROW_IF_FAILED(manifestReader->GetPackageId(&packageId)); + } + else + { + ComPtr<IAppxManifestReader> manifestReader; + THROW_IF_FAILED(m_packageReader->GetManifest(&manifestReader)); + THROW_IF_FAILED(manifestReader->GetPackageId(&packageId)); + } + + wil::unique_cotaskmem_string familyName; + THROW_IF_FAILED(packageId->GetPackageFamilyName(&familyName)); + + return Utility::ConvertToUTF8(familyName.get()); + } + + bool MsixInfo::IsNewerThan(const std::filesystem::path& otherManifest) + { + THROW_HR_IF(E_NOT_VALID_STATE, m_isBundle); + + ComPtr<IStream> otherStream; + THROW_IF_FAILED(SHCreateStreamOnFileEx(otherManifest.c_str(), + STGM_READ | STGM_SHARE_DENY_WRITE | STGM_FAILIFTHERE, 0, FALSE, nullptr, &otherStream)); + + ComPtr<IAppxManifestReader> otherReader; + GetManifestReader(otherStream.Get(), &otherReader); + + ComPtr<IAppxManifestReader> manifestReader; + THROW_IF_FAILED(m_packageReader->GetManifest(&manifestReader)); + + return (GetVersionFromManifestReader(manifestReader.Get()) > GetVersionFromManifestReader(otherReader.Get())); + } + + void MsixInfo::WriteToFile(std::string_view packageFile, const std::filesystem::path& target, IProgressCallback& progress) + { + std::wstring fileUTF16 = Utility::ConvertToUTF16(packageFile); + + ComPtr<IAppxFile> appxFile; + if (m_isBundle) + { + THROW_IF_FAILED(m_bundleReader->GetPayloadPackage(fileUTF16.c_str(), &appxFile)); + } + else + { + THROW_IF_FAILED(m_packageReader->GetPayloadFile(fileUTF16.c_str(), &appxFile)); + } + + WriteAppxFileToFile(appxFile.Get(), target, progress); + } + + void MsixInfo::WriteManifestToFile(const std::filesystem::path& target, IProgressCallback& progress) + { + ComPtr<IAppxFile> appxFile; + if (m_isBundle) + { + THROW_IF_FAILED(m_bundleReader->GetFootprintFile(APPX_BUNDLE_FOOTPRINT_FILE_TYPE_MANIFEST, &appxFile)); + } + else + { + THROW_IF_FAILED(m_packageReader->GetFootprintFile(APPX_FOOTPRINT_FILE_TYPE_MANIFEST, &appxFile)); + } + + WriteAppxFileToFile(appxFile.Get(), target, progress); + } +} diff --git a/src/AppInstallerCommonCore/Public/AppInstallerDeployment.h b/src/AppInstallerCommonCore/Public/AppInstallerDeployment.h @@ -7,9 +7,14 @@ namespace AppInstaller::Deployment { - // Calls winrt::Windows::Management::Deployment::PackageManager::RequestAddPackageAsync as a Future. + // Calls winrt::Windows::Management::Deployment::PackageManager::RequestAddPackageAsync void RequestAddPackageAsync( const winrt::Windows::Foundation::Uri& uri, winrt::Windows::Management::Deployment::DeploymentOptions options, IProgressCallback& callback); + + // Calls winrt::Windows::Management::Deployment::PackageManager::RemovePackageAsync, + // but *DOES NOT WAIT FOR A RESULT*. As this is used for removing an optional package + // we will simply complete our actions + void RemovePackageFireAndForget(winrt::hstring packageFullName); } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h b/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h @@ -6,6 +6,7 @@ #include <filesystem> #include <optional> #include <string> +#include <string_view> #include <vector> namespace AppInstaller::Utility @@ -19,4 +20,7 @@ namespace AppInstaller::Utility const std::filesystem::path& dest, IProgressCallback& progress, bool computeHash = false); -}- \ No newline at end of file + + // Determines if the given url is a remote location. + bool IsUrlRemote(std::string_view url); +} diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -18,3 +18,4 @@ #define APPINSTALLER_CLI_ERROR_SOURCES_INVALID ((HRESULT)0x8A15000B) #define APPINSTALLER_CLI_ERROR_SOURCE_NAME_ALREADY_EXISTS ((HRESULT)0x8A15000C) #define APPINSTALLER_CLI_ERROR_INVALID_SOURCE_TYPE ((HRESULT)0x8A15000D) +#define APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE ((HRESULT)0x8A15000E) diff --git a/src/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h b/src/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h @@ -1,6 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include <AppInstallerProgress.h> +#include <AppxPackaging.h> +#include <wrl/client.h> +#include <filesystem> +#include <string> +#include <string_view> +#include <vector> namespace AppInstaller::Msix { @@ -16,10 +23,15 @@ namespace AppInstaller::Msix IStream* inputStream, IAppxPackageReader** reader); + // Function to create an Appx manifest reader given the input file name. + void GetManifestReader( + IStream* inputStream, + IAppxManifestReader** reader); + // MsixInfo class handles all appx/msix related query. struct MsixInfo { - MsixInfo(const std::string& uriStr); + MsixInfo(std::string_view uriStr); MsixInfo(const MsixInfo&) = default; MsixInfo& operator=(const MsixInfo&) = default; @@ -35,6 +47,18 @@ namespace AppInstaller::Msix // Full content of AppxSignature.p7x std::vector<byte> GetSignature(); + // Gets the package family name. + std::string GetPackageFamilyName(); + + // Gets a value indicating whether the referenced info is newer than the given manifest. + bool IsNewerThan(const std::filesystem::path& otherManifest); + + // Writes the package file to the given path. + void WriteToFile(std::string_view packageFile, const std::filesystem::path& target, IProgressCallback& progress); + + // Writes the package's manifest to the given path. + void WriteManifestToFile(const std::filesystem::path& target, IProgressCallback& progress); + private: bool m_isBundle; Microsoft::WRL::ComPtr<IStream> m_stream; diff --git a/src/AppInstallerCommonCore/Public/AppInstallerProgress.h b/src/AppInstallerCommonCore/Public/AppInstallerProgress.h @@ -1,10 +1,22 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include <wil/resource.h> #include <atomic> +#include <functional> namespace AppInstaller { + // Forward declaration + struct ProgressCallback; + struct IProgressCallback; + + namespace details + { + // For SetCancellationFunction return. + inline void RemoveCancellationFunction(IProgressCallback* callback); + } + // The semantic meaning of the progress values. enum class ProgressType { @@ -18,12 +30,17 @@ namespace AppInstaller // Also enables the caller to request cancellation. struct IProgressCallback { + using CancelFunctionRemoval = wil::unique_any<IProgressCallback*, decltype(&details::RemoveCancellationFunction), details::RemoveCancellationFunction>; + // Called as progress is made. // If maximum is 0, the maximum is unknown. virtual void OnProgress(uint64_t current, uint64_t maximum, ProgressType type) = 0; // Returns a value indicating if the future has been cancelled. virtual bool IsCancelled() = 0; + + // Sets a cancellation function that will be called when the operation is to be cancelled. + [[nodiscard]] virtual CancelFunctionRemoval SetCancellationFunction(std::function<void()>&& f) = 0; }; // Implementation of IProgressCallback. @@ -46,9 +63,26 @@ namespace AppInstaller return m_cancelled.load(); } + [[nodiscard]] IProgressCallback::CancelFunctionRemoval SetCancellationFunction(std::function<void()>&& f) override + { + m_cancellationFunction = std::move(f); + if (m_cancellationFunction) + { + return IProgressCallback::CancelFunctionRemoval(this); + } + else + { + return {}; + } + } + void Cancel() { m_cancelled = true; + if (m_cancellationFunction) + { + m_cancellationFunction(); + } } IProgressCallback* GetCallback() @@ -59,5 +93,14 @@ namespace AppInstaller private: std::atomic<IProgressCallback*> m_callback = nullptr; std::atomic_bool m_cancelled = false; + std::function<void()> m_cancellationFunction; }; + + namespace details + { + inline void RemoveCancellationFunction(IProgressCallback* callback) + { + (void)callback->SetCancellationFunction(nullptr); + } + } } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h @@ -17,6 +17,9 @@ namespace AppInstaller::Runtime // Gets the path to the temp location. std::filesystem::path GetPathToTemp(); + // Gets the path to the local state location. + std::filesystem::path GetPathToLocalState(); + // Gets a stream containing the named setting's value, if present. // If the setting does not exist, returns an empty value. std::unique_ptr<std::istream> GetSettingStream(std::filesystem::path name); diff --git a/src/AppInstallerCommonCore/Runtime.cpp b/src/AppInstallerCommonCore/Runtime.cpp @@ -8,6 +8,10 @@ namespace AppInstaller::Runtime { namespace { + using namespace std::string_view_literals; + constexpr std::string_view s_AppDataDir_Settings = "Settings"; + constexpr std::string_view s_AppDataDir_State = "State"; + // Gets a boolean indicating whether the current process has identity. bool DoesCurrentProcessHaveIdentity() { @@ -25,15 +29,6 @@ namespace AppInstaller::Runtime THROW_HR_IF(E_INVALIDARG, !name.has_relative_path()); THROW_HR_IF(E_INVALIDARG, name.has_root_path()); THROW_HR_IF(E_INVALIDARG, !name.has_filename()); - -#ifndef AICLI_DISABLE_TEST_HOOKS - if (!s_Settings_TestHook_ForcedContainerPrepend.empty()) - { - std::filesystem::path result = s_Settings_TestHook_ForcedContainerPrepend; - result /= name; - name = std::move(result); - } -#endif } // Gets the container within LocalSettings for the given path. @@ -41,7 +36,21 @@ namespace AppInstaller::Runtime { auto result = winrt::Windows::Storage::ApplicationData::Current().LocalSettings(); - for (const auto& part : name.parent_path()) + std::filesystem::path pathToUse; + +#ifndef AICLI_DISABLE_TEST_HOOKS + if (!s_Settings_TestHook_ForcedContainerPrepend.empty()) + { + pathToUse = s_Settings_TestHook_ForcedContainerPrepend; + pathToUse /= name; + } + else +#endif + { + pathToUse = name; + } + + for (const auto& part : pathToUse.parent_path()) { auto partHstring = winrt::to_hstring(part.c_str()); result = result.CreateContainer(partHstring, winrt::Windows::Storage::ApplicationDataCreateDisposition::Always); @@ -67,22 +76,34 @@ namespace AppInstaller::Runtime std::filesystem::path result = localAppDataPath; result /= "Microsoft/AppInstaller"; + +#ifndef AICLI_DISABLE_TEST_HOOKS + if (!s_Settings_TestHook_ForcedContainerPrepend.empty()) + { + result /= s_Settings_TestHook_ForcedContainerPrepend; + } +#endif + return result; } - // Gets the path to the settings root. + // Gets the path to the app data relative directory. // Creates the directory if it does not already exist. - std::filesystem::path GetPathToSettingsRoot() + std::filesystem::path GetPathToAppDataDir(const std::filesystem::path& relative) { + THROW_HR_IF(E_INVALIDARG, !relative.has_relative_path()); + THROW_HR_IF(E_INVALIDARG, relative.has_root_path()); + THROW_HR_IF(E_INVALIDARG, !relative.has_filename()); + std::filesystem::path result = GetPathToAppDataRoot(); - result /= "Settings"; + result /= relative; if (std::filesystem::exists(result)) { if (!std::filesystem::is_directory(result)) { // STATUS_NOT_A_DIRECTORY: A requested opened file is not a directory. - THROW_NTSTATUS_MSG(0xC0000103, "Settings is not a directory"); + THROW_NTSTATUS_MSG(0xC0000103, "AppData location is not a directory"); } } else @@ -97,7 +118,7 @@ namespace AppInstaller::Runtime // Creates the directory if it does not already exist. std::filesystem::path GetPathToSettings(const std::filesystem::path& name) { - std::filesystem::path result = GetPathToAppDataRoot(); + std::filesystem::path result = GetPathToAppDataDir(s_AppDataDir_Settings); if (name.has_parent_path()) { result /= name.parent_path(); @@ -159,6 +180,27 @@ namespace AppInstaller::Runtime } } + std::filesystem::path GetPathToLocalState() + { + if (IsRunningInPackagedContext()) + { + std::filesystem::path result = winrt::Windows::Storage::ApplicationData::Current().LocalFolder().Path().c_str(); + +#ifndef AICLI_DISABLE_TEST_HOOKS + if (!s_Settings_TestHook_ForcedContainerPrepend.empty()) + { + result /= s_Settings_TestHook_ForcedContainerPrepend; + } +#endif + + return result; + } + else + { + return GetPathToAppDataDir(s_AppDataDir_State); + } + } + std::unique_ptr<std::istream> GetSettingStream(std::filesystem::path name) { ValidateSettingNamePath(name); diff --git a/src/AppInstallerCommonCore/pch.h b/src/AppInstallerCommonCore/pch.h @@ -6,6 +6,7 @@ #include <Windows.h> #include <appmodel.h> #include <WinInet.h> +#include <Shlwapi.h> #include "TraceLogging.h" @@ -15,6 +16,7 @@ #include <wil/safecast.h> #include <wil/resource.h> +#include <winrt/Windows.ApplicationModel.h> #include <winrt/Windows.Foundation.h> #include <winrt/Windows.Foundation.Collections.h> #include <winrt/Windows.Security.Cryptography.h> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -122,9 +122,9 @@ <ClCompile> <Optimization>Disabled</Optimization> <PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">$(ProjectDir);$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(ProjectDir);$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(ProjectDir);$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</TreatWarningAsError> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</TreatWarningAsError> @@ -139,7 +139,7 @@ <ItemDefinitionGroup Condition="'$(Platform)'=='Win32'"> <ClCompile> <PreprocessorDefinitions>WIN32;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(ProjectDir);$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</TreatWarningAsError> </ClCompile> <Link> @@ -152,10 +152,10 @@ <FunctionLevelLinking>true</FunctionLevelLinking> <IntrinsicFunctions>true</IntrinsicFunctions> <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD</PreprocessorDefinitions> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">$(ProjectDir);$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">$(ProjectDir);$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(ProjectDir);$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> - <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(ProjectDir);$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\YamlCppLib\yaml-cpp\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> </ClCompile> <Link> <EnableCOMDATFolding>true</EnableCOMDATFolding> @@ -171,7 +171,7 @@ <ClInclude Include="Manifest\Manifest.h" /> <ClInclude Include="Manifest\ManifestInstaller.h" /> <ClInclude Include="Manifest\ManifestLocalization.h" /> - <ClInclude Include="Microsoft\PreIndexedSource.h" /> + <ClInclude Include="Microsoft\PreIndexedPackageSourceFactory.h" /> <ClInclude Include="Microsoft\Schema\1_0\ChannelTable.h" /> <ClInclude Include="Microsoft\Schema\1_0\CommandsTable.h" /> <ClInclude Include="Microsoft\Schema\1_0\IdTable.h" /> @@ -199,7 +199,7 @@ <ClCompile Include="Manifest\Manifest.cpp" /> <ClCompile Include="Manifest\ManifestInstaller.cpp" /> <ClCompile Include="Manifest\ManifestLocalization.cpp" /> - <ClCompile Include="Microsoft\PreIndexedSource.cpp" /> + <ClCompile Include="Microsoft\PreIndexedPackageSourceFactory.cpp" /> <ClCompile Include="Microsoft\Schema\1_0\Interface.cpp" /> <ClCompile Include="Microsoft\Schema\1_0\ManifestTable.cpp" /> <ClCompile Include="Microsoft\Schema\1_0\OneToManyTable.cpp" /> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -102,12 +102,12 @@ <ClInclude Include="Public\AppInstallerRepositorySearch.h"> <Filter>Public</Filter> </ClInclude> - <ClInclude Include="Microsoft\PreIndexedSource.h"> - <Filter>Microsoft</Filter> - </ClInclude> <ClInclude Include="SourceFactory.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="Microsoft\PreIndexedPackageSourceFactory.h"> + <Filter>Microsoft</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -155,7 +155,7 @@ <ClCompile Include="RepositorySource.cpp"> <Filter>Source Files</Filter> </ClCompile> - <ClCompile Include="Microsoft\PreIndexedSource.cpp"> + <ClCompile Include="Microsoft\PreIndexedPackageSourceFactory.cpp"> <Filter>Microsoft</Filter> </ClCompile> </ItemGroup> diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp @@ -0,0 +1,259 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "Microsoft/PreIndexedPackageSourceFactory.h" + +using namespace std::string_literals; +using namespace std::string_view_literals; + +namespace AppInstaller::Repository::Microsoft +{ + namespace + { + static constexpr std::string_view s_PreIndexedPackageSourceFactory_PackageFileName = "index.msix"sv; + static constexpr std::string_view s_PreIndexedPackageSourceFactory_AppxManifestFileName = "AppxManifest.xml"sv; + static constexpr std::string_view s_PreIndexedPackageSourceFactory_IndexFileName = "index.db"sv; + + // Construct the package location from the given details. + // Currently expects that the arg is an https uri pointing to the root of the data. + std::string GetPackageLocation(const SourceDetails& details) + { + THROW_HR_IF(E_INVALIDARG, details.Arg.empty()); + std::string result = details.Arg; + if (result.back() != '/') + { + result += '/'; + } + result += s_PreIndexedPackageSourceFactory_PackageFileName; + return result; + } + + // Gets the package family name from the details. + std::string GetPackageFamilyNameFromDetails(const SourceDetails& details) + { + THROW_HR_IF(E_UNEXPECTED, details.Data.empty()); + return details.Data; + } + + // Creates a name for the cross process reader-writer lock given the details. + std::string CreateNameForCPRWL(const SourceDetails& details) + { + // The only relevant data is the package family name + return "PreIndexedSourceCPRWL_"s + GetPackageFamilyNameFromDetails(details); + } + + // The base class for a package that comes from a preindexed packaged source. + struct PreIndexedFactoryBase : public ISourceFactory + { + bool IsInitialized(const SourceDetails& details) override final + { + return !details.Data.empty(); + } + + std::unique_ptr<ISource> Create(const SourceDetails& details) override final + { + THROW_HR_IF(E_INVALIDARG, details.Type != PreIndexedPackageSourceFactory::Type()); + THROW_HR_IF(E_UNEXPECTED, !IsInitialized(details)); + + auto lock = Synchronization::CrossProcessReaderWriteLock::LockForRead(CreateNameForCPRWL(details)); + return CreateInternal(details, std::move(lock)); + } + + virtual std::unique_ptr<ISource> CreateInternal(const SourceDetails& details, Synchronization::CrossProcessReaderWriteLock&& lock) = 0; + + void Update(SourceDetails& details, IProgressCallback& progress) override final + { + if (details.Type.empty()) + { + // With more than one source implementation, we will probably need to probe first + details.Type = PreIndexedPackageSourceFactory::Type(); + AICLI_LOG(Repo, Info, << "Initializing source type: " << details.Name << " => " << details.Type); + } + else + { + THROW_HR_IF(E_INVALIDARG, details.Type != PreIndexedPackageSourceFactory::Type()); + } + + std::string packageLocation = GetPackageLocation(details); + + if (!IsInitialized(details)) + { + AICLI_LOG(Repo, Info, << "Initializing source from: " << details.Name << " => " << packageLocation); + + // If not initialized, we need to open the package and get the family name. + Msix::MsixInfo packageInfo(packageLocation); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, packageInfo.GetIsBundle()); + details.Data = packageInfo.GetPackageFamilyName(); + + AICLI_LOG(Repo, Info, << "Found package family name: " << details.Name << " => " << details.Data); + } + + auto lock = Synchronization::CrossProcessReaderWriteLock::LockForWrite(CreateNameForCPRWL(details)); + + UpdateInternal(packageLocation, details, progress); + + details.LastUpdateTime = std::chrono::system_clock::now(); + } + + virtual void UpdateInternal(std::string packageLocation, SourceDetails& details, IProgressCallback& progress) = 0; + + void Remove(const SourceDetails& details, IProgressCallback& progress) override final + { + THROW_HR_IF(E_INVALIDARG, details.Type != PreIndexedPackageSourceFactory::Type()); + auto lock = Synchronization::CrossProcessReaderWriteLock::LockForWrite(CreateNameForCPRWL(details)); + + RemoveInternal(details, progress); + } + + virtual void RemoveInternal(const SourceDetails& details, IProgressCallback&) = 0; + }; + + // Source factory for running within a packaged context + struct PackagedContextFactory : public PreIndexedFactoryBase + { + // *Should only be called when under a CrossProcessReaderWriteLock* + auto GetPackageFromDetails(const SourceDetails& details) + { + using Package = winrt::Windows::ApplicationModel::Package; + + std::wstring packageFamilyName = Utility::ConvertToUTF16(GetPackageFamilyNameFromDetails(details)); + + Package currentPackage = Package::Current(); + auto dependencies = currentPackage.Dependencies(); + for (uint32_t i = 0; i < dependencies.Size(); ++i) + { + Package package = dependencies.GetAt(i); + if (package.Id().FamilyName() == packageFamilyName) + { + if (package.IsOptional()) + { + return package; + } + else + { + AICLI_LOG(Repo, Error, << "Source references a non-optional package: " << details.Name << " => " << GetPackageFamilyNameFromDetails(details)); + return Package{ nullptr }; + } + } + } + AICLI_LOG(Repo, Error, << "Source references an unknown package: " << details.Name << " => " << GetPackageFamilyNameFromDetails(details)); + return Package{ nullptr }; + } + + std::unique_ptr<ISource> CreateInternal(const SourceDetails& details, Synchronization::CrossProcessReaderWriteLock&& lock) override + { + UNREFERENCED_PARAMETER(details); + UNREFERENCED_PARAMETER(lock); + THROW_HR(E_NOTIMPL); + } + + void UpdateInternal(std::string packageLocation, SourceDetails&, IProgressCallback& progress) override + { + winrt::Windows::Foundation::Uri uri(Utility::ConvertToUTF16(packageLocation)); + Deployment::RequestAddPackageAsync(uri, winrt::Windows::Management::Deployment::DeploymentOptions::None, progress); + } + + void RemoveInternal(const SourceDetails& details, IProgressCallback&) override + { + // Get the package referenced by the details + auto optionalPackage = GetPackageFromDetails(details); + if (!optionalPackage) + { + AICLI_LOG(Repo, Info, << "Package not found by family name " << details.Data); + return; + } + + // Begin package removal, but let it run its course without waiting. + // This pattern is required due to the inability to use SetInUseAsync from a full trust process. + AICLI_LOG(Repo, Info, << "Removing package " << Utility::ConvertToUTF8(optionalPackage.Id().FullName())); + Deployment::RemovePackageFireAndForget(optionalPackage.Id().FullName()); + } + }; + + // Source factory for running outside of a package. + struct DesktopContextFactory : public PreIndexedFactoryBase + { + // Constructs the location that we will write files to. + std::filesystem::path GetStatePathFromDetails(const SourceDetails& details) + { + std::filesystem::path result = Runtime::GetPathToLocalState(); + result /= PreIndexedPackageSourceFactory::Type(); + result /= GetPackageFamilyNameFromDetails(details); + return result; + } + + std::unique_ptr<ISource> CreateInternal(const SourceDetails& details, Synchronization::CrossProcessReaderWriteLock&& lock) override + { + UNREFERENCED_PARAMETER(details); + UNREFERENCED_PARAMETER(lock); + THROW_HR(E_NOTIMPL); + } + + void UpdateInternal(std::string packageLocation, SourceDetails& details, IProgressCallback& progress) override + { + // We will extract the manifest and index files directly to this location + std::filesystem::path packageState = GetStatePathFromDetails(details); + std::filesystem::create_directories(packageState); + + Msix::MsixInfo packageInfo(packageLocation); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, packageInfo.GetIsBundle()); + + if (progress.IsCancelled()) + { + AICLI_LOG(Repo, Info, << "Cancelling update upon request"); + return; + } + + std::filesystem::path manifestPath = packageState / s_PreIndexedPackageSourceFactory_AppxManifestFileName; + std::filesystem::path indexPath = packageState / s_PreIndexedPackageSourceFactory_IndexFileName; + + if (std::filesystem::exists(manifestPath) && std::filesystem::exists(indexPath)) + { + // If we already have a manifest, use it to determine if we need to update or not. + if (!packageInfo.IsNewerThan(manifestPath)) + { + AICLI_LOG(Repo, Info, << "Remote source data was not newer than existing, no update needed"); + return; + } + } + + if (progress.IsCancelled()) + { + AICLI_LOG(Repo, Info, << "Cancelling update upon request"); + return; + } + + packageInfo.WriteToFile(s_PreIndexedPackageSourceFactory_IndexFileName, indexPath, progress); + packageInfo.WriteManifestToFile(manifestPath, progress); + } + + void RemoveInternal(const SourceDetails& details, IProgressCallback&) override + { + std::filesystem::path packageState = GetStatePathFromDetails(details); + + if (!std::filesystem::exists(packageState)) + { + AICLI_LOG(Repo, Info, << "No state found for source: " << packageState.u8string()); + } + else + { + AICLI_LOG(Repo, Info, << "Removing state found for source: " << packageState.u8string()); + std::filesystem::remove_all(packageState); + } + } + }; + } + + std::unique_ptr<ISourceFactory> PreIndexedPackageSourceFactory::Create() + { + if (Runtime::IsRunningInPackagedContext()) + { + return std::make_unique<PackagedContextFactory>(); + } + else + { + return std::make_unique<DesktopContextFactory>(); + } + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.h b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.h @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Public/AppInstallerRepositorySource.h" +#include "SourceFactory.h" + +#include <string_view> + +namespace AppInstaller::Repository::Microsoft +{ + // A source where the index is precomputed and stored on a server within an optional MSIX package. + // In addition, the manifest files are also individually available on the server. + // Arg :: Expected to be a fully qualified path to the root of the data. + // This can be a web location such as https://somewhere/ or a local file share \\somewhere\ + // Under this path there must exist an MSIX package called "index.msix". + // This must have a file called "index.db" contained within, which is a SQLiteIndex. + // The index's paths refer to relative locations under the Arg value. + // Data :: The package family name of the package at Arg + /index.msix. + struct PreIndexedPackageSourceFactory + { + // Get the type string for this source. + static constexpr std::string_view Type() + { + using namespace std::string_view_literals; + return "Microsoft.PreIndexed.Package"sv; + } + + // Creates a source factory for this type. + static std::unique_ptr<ISourceFactory> Create(); + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedSource.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedSource.cpp @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#pragma once -#include "pch.h" -#include "Microsoft/PreIndexedSource.h" - -namespace AppInstaller::Repository::Microsoft -{ - namespace - { - struct PreIndexedSourceFactory : public ISourceFactory - { - std::unique_ptr<ISource> Create(const SourceDetails& details) override - { - UNREFERENCED_PARAMETER(details); - THROW_HR(E_NOTIMPL); - } - - void Update(SourceDetails& details) override - { - UNREFERENCED_PARAMETER(details); - THROW_HR(E_NOTIMPL); - } - - void Remove(const SourceDetails& details) override - { - UNREFERENCED_PARAMETER(details); - THROW_HR(E_NOTIMPL); - } - }; - } - - std::unique_ptr<ISourceFactory> PreIndexedSource::CreateFactory() - { - return std::make_unique<PreIndexedSourceFactory>(); - } - - const SourceDetails& PreIndexedSource::GetDetails() const - { - THROW_HR(E_NOTIMPL); - } - - SearchResult PreIndexedSource::Search(const SearchRequest& request) const - { - UNREFERENCED_PARAMETER(request); - THROW_HR(E_NOTIMPL); - } -} diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedSource.h b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedSource.h @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#pragma once -#include "Public/AppInstallerRepositorySource.h" -#include "SourceFactory.h" - -#include <string_view> - -namespace AppInstaller::Repository::Microsoft -{ - // A source where the index is precomputed and stored on a server within an optional MSIX package. - // In addition, the manifest files are also individually available on the server. - struct PreIndexedSource : public ISource - { - PreIndexedSource(const PreIndexedSource&) = delete; - PreIndexedSource& operator=(const PreIndexedSource&) = delete; - - PreIndexedSource(PreIndexedSource&&) = default; - PreIndexedSource& operator=(PreIndexedSource&&) = default; - - // Get the type string for this source. - static constexpr std::string_view Type() - { - using namespace std::string_view_literals; - return "Microsoft.PreIndexed"sv; - } - - // Creates a source factory for this type. - static std::unique_ptr<ISourceFactory> CreateFactory(); - - // ISource - - // Get the source's details. - const SourceDetails& GetDetails() const override; - - // Execute a search on the source. - SearchResult Search(const SearchRequest& request) const override; - }; -} diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once -#include <Public/AppInstallerRepositorySearch.h> +#include <AppInstallerRepositorySearch.h> +#include <AppInstallerProgress.h> #include <chrono> #include <memory> @@ -47,17 +48,17 @@ namespace AppInstaller::Repository std::vector<SourceDetails> GetSources(); // Adds a new source for the user. - void AddSource(std::string name, std::string type, std::string arg); + void AddSource(std::string name, std::string type, std::string arg, IProgressCallback& progress); // Opens an existing source. // Passing an empty string as the name of the source will return a source that aggregates all others. - std::unique_ptr<ISource> OpenSource(std::string_view name = {}); + std::unique_ptr<ISource> OpenSource(std::string_view name, IProgressCallback& progress); // Updates an existing source. // Return value indicates whether the named source was found. - bool UpdateSource(std::string_view name); + bool UpdateSource(std::string_view name, IProgressCallback& progress); // Removes an existing source. // Return value indicates whether the named source was found. - bool RemoveSource(std::string_view name); + bool RemoveSource(std::string_view name, IProgressCallback& progress); } diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -4,7 +4,7 @@ #include "Public/AppInstallerRepositorySource.h" #include "SourceFactory.h" -#include "Microsoft/PreIndexedSource.h" +#include "Microsoft/PreIndexedPackageSourceFactory.h" namespace AppInstaller::Repository { @@ -173,31 +173,50 @@ namespace AppInstaller::Repository // For now, enable an empty type to represent the only one we have. if (type.empty() || - Utility::CaseInsensitiveEquals(Microsoft::PreIndexedSource::Type(), type)) + Utility::CaseInsensitiveEquals(Microsoft::PreIndexedPackageSourceFactory::Type(), type)) { - return Microsoft::PreIndexedSource::CreateFactory(); + return Microsoft::PreIndexedPackageSourceFactory::Create(); } THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_SOURCE_TYPE); } + bool CheckIfInitializedFromDetails(const SourceDetails& details) + { + return GetFactoryForType(details.Type)->IsInitialized(details); + } + std::unique_ptr<ISource> CreateSourceFromDetails(const SourceDetails& details) { return GetFactoryForType(details.Type)->Create(details); } - void UpdateSourceFromDetails(SourceDetails& details) + void UpdateSourceFromDetails(SourceDetails& details, IProgressCallback& progress) { - GetFactoryForType(details.Type)->Update(details); + GetFactoryForType(details.Type)->Update(details, progress); } - void RemoveSourceFromDetails(const SourceDetails& details) + void RemoveSourceFromDetails(const SourceDetails& details, IProgressCallback& progress) { - GetFactoryForType(details.Type)->Remove(details); + auto factory = GetFactoryForType(details.Type); + + if (factory->IsInitialized(details)) + { + factory->Remove(details, progress); + } + else + { + AICLI_LOG(Repo, Info, << "Uninitialized source being removed, making it a no-op: " << details.Name); + } } } - void AddSource(std::string name, std::string type, std::string arg) + std::vector<SourceDetails> GetSources() + { + return GetSourcesFromSetting(s_RepositorySettings_UserSources); + } + + void AddSource(std::string name, std::string type, std::string arg, IProgressCallback& progress) { THROW_HR_IF(E_INVALIDARG, name.empty()); @@ -215,7 +234,7 @@ namespace AppInstaller::Repository details.Arg = std::move(arg); details.LastUpdateTime = Utility::ConvertUnixEpochToSystemClock(0); - UpdateSourceFromDetails(details); + UpdateSourceFromDetails(details, progress); AICLI_LOG(Repo, Info, << "Source created with extra data: " << details.Data); @@ -225,13 +244,12 @@ namespace AppInstaller::Repository SetSourcesToSetting(s_RepositorySettings_UserSources, currentSources); } - std::unique_ptr<ISource> OpenSource(std::string_view name) + std::unique_ptr<ISource> OpenSource(std::string_view name, IProgressCallback& progress) { + std::vector<SourceDetails> currentSources = GetSources(); + if (name.empty()) { - // TODO: Create aggregate source here. For now, just get the first in the list. - std::vector<SourceDetails> currentSources = GetSources(); - if (currentSources.empty()) { AICLI_LOG(Repo, Info, << "Default source requested, but no sources configured"); @@ -239,13 +257,13 @@ namespace AppInstaller::Repository } else { + // TODO: Create aggregate source here. For now, just get the first in the list. AICLI_LOG(Repo, Info, << "Default source requested, using first source: " << currentSources[0].Name); - return CreateSourceFromDetails(currentSources[0]); + return OpenSource(currentSources[0].Name, progress); } } else { - std::vector<SourceDetails> currentSources = GetSources(); auto itr = FindSourceByName(currentSources, name); if (itr == currentSources.end()) @@ -256,17 +274,18 @@ namespace AppInstaller::Repository else { AICLI_LOG(Repo, Info, << "Named source requested, found: " << itr->Name); + if (!CheckIfInitializedFromDetails(*itr)) + { + AICLI_LOG(Repo, Info, << "Source needs to be initialized during open: " << itr->Name); + UpdateSourceFromDetails(*itr, progress); + SetSourcesToSetting(s_RepositorySettings_UserSources, currentSources); + } return CreateSourceFromDetails(*itr); } } } - std::vector<SourceDetails> GetSources() - { - return GetSourcesFromSetting(s_RepositorySettings_UserSources); - } - - bool UpdateSource(std::string_view name) + bool UpdateSource(std::string_view name, IProgressCallback& progress) { THROW_HR_IF(E_INVALIDARG, name.empty()); @@ -281,14 +300,14 @@ namespace AppInstaller::Repository else { AICLI_LOG(Repo, Info, << "Named source to be updated, found: " << itr->Name); - UpdateSourceFromDetails(*itr); + UpdateSourceFromDetails(*itr, progress); SetSourcesToSetting(s_RepositorySettings_UserSources, currentSources); return true; } } - bool RemoveSource(std::string_view name) + bool RemoveSource(std::string_view name, IProgressCallback& progress) { THROW_HR_IF(E_INVALIDARG, name.empty()); @@ -303,7 +322,7 @@ namespace AppInstaller::Repository else { AICLI_LOG(Repo, Info, << "Named source to be removed, found: " << itr->Name); - RemoveSourceFromDetails(*itr); + RemoveSourceFromDetails(*itr, progress); currentSources.erase(itr); SetSourcesToSetting(s_RepositorySettings_UserSources, currentSources); diff --git a/src/AppInstallerRepositoryCore/SourceFactory.h b/src/AppInstallerRepositoryCore/SourceFactory.h @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once -#include <Public/AppInstallerRepositorySource.h> +#include <AppInstallerRepositorySource.h> +#include <AppInstallerProgress.h> #include <memory> @@ -13,13 +14,16 @@ namespace AppInstaller::Repository { virtual ~ISourceFactory() = default; + // Returns a value indicating whether the source details reference a source that is properly initialized. + virtual bool IsInitialized(const SourceDetails& details) = 0; + // Creates a source object from the given details. virtual std::unique_ptr<ISource> Create(const SourceDetails& details) = 0; // Updates the source from the given details, writing back to the details any changes. - virtual void Update(SourceDetails& details) = 0; + virtual void Update(SourceDetails& details, IProgressCallback& progress) = 0; // Removes the source from the given details. - virtual void Remove(const SourceDetails& details) = 0; + virtual void Remove(const SourceDetails& details, IProgressCallback& progress) = 0; }; } diff --git a/src/AppInstallerRepositoryCore/pch.h b/src/AppInstallerRepositoryCore/pch.h @@ -6,18 +6,23 @@ #include <windows.h> #include <AppInstallerDateTime.h> +#include <AppInstallerDeployment.h> #include <AppInstallerErrors.h> #include <AppInstallerLogging.h> +#include <AppInstallerMsixInfo.h> #include <AppInstallerRuntime.h> #include <AppInstallerSHA256.h> #include <AppInstallerStrings.h> +#include <AppInstallerSynchronization.h> #include <yaml-cpp/yaml.h> #include <wil/result_macros.h> #include <winsqlite/winsqlite3.h> +#include <winrt/Windows.ApplicationModel.h> #include <winrt/Windows.Foundation.h> +#include <winrt/Windows.Foundation.Collections.h> #include <algorithm> #include <filesystem>