diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 53175f0afa..982fc5b913 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -29,6 +29,7 @@ appdata appinstallertest applic appname +appone appshutdown APPTERMINATION archs @@ -339,6 +340,7 @@ megamorf microsoftentraid microsoftentraidforazureblobstorage midl +migratepintable minidump MINORVERSION missingdependency @@ -431,6 +433,7 @@ Pherson pid pidl pidlist +pintable PKCS pkgmgr pkindex diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 77aabce02b..f5ce805ac7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -133,6 +133,105 @@ void WorkflowTask(Execution::Context& context) - Parsing in `AppInstallerCommonCore/Manifest/` - Multi-file manifests: installer, locale, version, defaultLocale +## SQLite Statement Builder + +**Always use `AppInstaller::SQLite::Builder::StatementBuilder` when writing SQLite database code. Never write raw SQL strings.** + +The builder is in `` (namespace `AppInstaller::SQLite::Builder`). It generates type-safe, parameterized SQL and ensures symbolic names are used for tables and columns throughout. + +### Key conventions + +- Define table and column names as `constexpr std::string_view` constants, then pass them to the builder. +- Use `ColumnBuilder` with chained modifiers (`.NotNull()`, `.Unique()`, `.Default(value)`) when creating tables. +- Use `IntegerPrimaryKey()` for auto-increment rowid primary keys. +- Use `Unbound` as a placeholder and bind values later via `Statement::Bind()`, or pass values directly to have them bound automatically. + +### Common operations + +```cpp +using namespace AppInstaller::SQLite::Builder; + +// Symbolic names +static constexpr std::string_view s_MyTable = "my_table"sv; +static constexpr std::string_view s_Col_Id = "id"sv; +static constexpr std::string_view s_Col_Name = "name"sv; +static constexpr std::string_view s_Col_Value = "value"sv; + +// CREATE TABLE +StatementBuilder builder; +builder.CreateTable(s_MyTable).Columns({ + IntegerPrimaryKey(), + ColumnBuilder(s_Col_Name, Type::Text).NotNull().Unique(), + ColumnBuilder(s_Col_Value, Type::Int64).NotNull().Default(0), +}); +builder.Execute(connection); + +// SELECT +StatementBuilder builder; +builder.Select({ s_Col_Id, s_Col_Name }) + .From(s_MyTable) + .Where(s_Col_Name).Equals(nameValue); +auto stmt = builder.Prepare(connection); +while (stmt.Step()) { /* stmt.GetColumn(index) */ } + +// INSERT +StatementBuilder builder; +builder.InsertInto(s_MyTable) + .Columns({ s_Col_Name, s_Col_Value }) + .Values(nameValue, intValue); +builder.Execute(connection); + +// UPDATE +StatementBuilder builder; +builder.Update(s_MyTable).Set() + .Column(s_Col_Value).AssignValue(newValue) + .Where(s_Col_Id).Equals(rowId); +builder.Execute(connection); + +// DELETE +StatementBuilder builder; +builder.DeleteFrom(s_MyTable) + .Where(s_Col_Id).Equals(rowId); +builder.Execute(connection); + +// ALTER TABLE – add a column +StatementBuilder builder; +builder.AlterTable(s_MyTable).Add(s_Col_NewCol, Type::Text).NotNull().Default(0); +builder.Execute(connection); +``` + +### Nullable values: `Equals()` vs `AssignValue()` + +Both accept `std::optional`, but they behave differently when the optional is empty and must be used in the right context: + +| Method | Empty optional emits | Use in | +|---|---|---| +| `Equals(optional)` | `IS NULL` | **WHERE** / filter clauses | +| `AssignValue(optional)` | `= ?` (binds NULL) | **UPDATE SET** assignments | + +Using `Equals(optional)` in an UPDATE SET clause is a bug — SQLite does not accept `col = IS NULL`. + +```cpp +// ✅ Correct: Equals for WHERE filter, AssignValue for SET assignment +std::optional maybeEpoch = ...; +std::optional maybeNote = ...; + +builder.Update(s_MyTable).Set() + .Column(s_Col_Name).AssignValue(requiredName) // non-optional: AssignValue in SET + .Column(s_Col_Epoch).AssignValue(maybeEpoch) // nullable: must use AssignValue in SET + .Column(s_Col_Note).AssignValue(maybeNote) // nullable: must use AssignValue in SET + .Where(s_Col_Id).Equals(rowId); // filter: Equals is correct here + +// ✅ Correct: Equals(optional) in WHERE — emits "IS NULL" when empty +builder.Select(s_Col_Id).From(s_MyTable) + .Where(s_Col_Note).Equals(maybeNote); // → "WHERE note IS NULL" when empty +``` + +### Execution + +- `builder.Execute(connection)` — prepares, binds, and runs a statement that returns no rows. +- `builder.Prepare(connection)` — returns a `SQLite::Statement`; call `.Step()` to iterate, `.GetColumn(index)` to read values. + ## Naming Conventions - **Namespace structure**: `AppInstaller::[::]` diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index 99699bdb0a..f22524c8b2 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -203,6 +203,8 @@ namespace AppInstaller::CLI return { type, "blocking"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::PinType }; case Execution::Args::Type::PinInstalled: return { type, "installed"_liv, ArgTypeCategory::None }; + case Execution::Args::Type::PinNote: + return { type, "note"_liv, ArgTypeCategory::None }; // Error command case Execution::Args::Type::ErrorInput: diff --git a/src/AppInstallerCLICore/Commands/PinCommand.cpp b/src/AppInstallerCLICore/Commands/PinCommand.cpp index 9d9219d68f..f47e81055d 100644 --- a/src/AppInstallerCLICore/Commands/PinCommand.cpp +++ b/src/AppInstallerCLICore/Commands/PinCommand.cpp @@ -23,6 +23,7 @@ namespace AppInstaller::CLI std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), + std::make_unique(FullName()), }); } @@ -65,6 +66,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Force), Argument{ Args::Type::BlockingPin, Resource::String::PinAddBlockingArgumentDescription, ArgumentType::Flag }, Argument{ Args::Type::PinInstalled, Resource::String::PinInstalledArgumentDescription, ArgumentType::Flag }, + Argument{ Args::Type::PinNote, Resource::String::PinNoteArgumentDescription, ArgumentType::Standard }, }; } @@ -343,4 +345,36 @@ namespace AppInstaller::CLI Workflow::ReportPins; } } + + std::vector PinShowCommand::GetArguments() const + { + return { + Argument::ForType(Args::Type::Query), + Argument::ForType(Args::Type::Id), + Argument::ForType(Args::Type::Name), + Argument::ForType(Args::Type::Exact), + }; + } + + Resource::LocString PinShowCommand::ShortDescription() const + { + return { Resource::String::PinShowCommandShortDescription }; + } + + Resource::LocString PinShowCommand::LongDescription() const + { + return { Resource::String::PinShowCommandLongDescription }; + } + + Utility::LocIndView PinShowCommand::HelpLink() const + { + return s_PinCommand_HelpLink; + } + + void PinShowCommand::ExecuteInternal(Execution::Context& context) const + { + context << + Workflow::OpenPinningIndex(/* readOnly */ true) << + Workflow::ShowPinDetails; + } } diff --git a/src/AppInstallerCLICore/Commands/PinCommand.h b/src/AppInstallerCLICore/Commands/PinCommand.h index 285e66eaa7..2852f26bc0 100644 --- a/src/AppInstallerCLICore/Commands/PinCommand.h +++ b/src/AppInstallerCLICore/Commands/PinCommand.h @@ -87,4 +87,19 @@ namespace AppInstaller::CLI protected: void ExecuteInternal(Execution::Context& context) const override; }; + + struct PinShowCommand final : public Command + { + PinShowCommand(std::string_view parent) : Command("show", parent) {} + + std::vector GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + Utility::LocIndView HelpLink() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + }; } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index 675576a9d7..947aa7c6fd 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -122,6 +122,7 @@ namespace AppInstaller::CLI::Execution GatedVersion, // Differs from Version in that this supports wildcards BlockingPin, PinInstalled, + PinNote, // User-provided note to attach to a pin // Error command ErrorInput, diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index fa369b8ae5..a7395f6200 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -530,6 +530,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PinCannotOpenIndex); WINGET_DEFINE_RESOURCE_STRINGID(PinCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(PinDateAdded); WINGET_DEFINE_RESOURCE_STRINGID(PinDoesNotExist); WINGET_DEFINE_RESOURCE_STRINGID(PinExistsOverwriting); WINGET_DEFINE_RESOURCE_STRINGID(PinExistsUseForceArg); @@ -537,6 +538,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PinInstalledSource); WINGET_DEFINE_RESOURCE_STRINGID(PinListCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinListCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(PinNote); + WINGET_DEFINE_RESOURCE_STRINGID(PinNoteArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinNoPinsExist); WINGET_DEFINE_RESOURCE_STRINGID(PinRemoveCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinRemoveCommandShortDescription); @@ -546,6 +549,15 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PinResetSuccessful); WINGET_DEFINE_RESOURCE_STRINGID(PinResettingAll); WINGET_DEFINE_RESOURCE_STRINGID(PinResetUseForceArg); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelDateAdded); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelId); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelNote); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelSource); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelType); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelVersion); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowNoMatchFound); WINGET_DEFINE_RESOURCE_STRINGID(PinType); WINGET_DEFINE_RESOURCE_STRINGID(PinVersion); WINGET_DEFINE_RESOURCE_STRINGID(PlatformArgumentDescription); diff --git a/src/AppInstallerCLICore/Workflows/PinFlow.cpp b/src/AppInstallerCLICore/Workflows/PinFlow.cpp index ace205fc44..3f7440e2a4 100644 --- a/src/AppInstallerCLICore/Workflows/PinFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PinFlow.cpp @@ -3,7 +3,9 @@ #include "pch.h" #include "Resources.h" #include "PinFlow.h" +#include "ShowFlow.h" #include "TableOutput.h" +#include #include #include #include @@ -197,9 +199,18 @@ namespace AppInstaller::CLI::Workflow if (!pinsToAddOrUpdate.empty()) { - for (const auto& pin : pinsToAddOrUpdate) + auto pinTime = std::chrono::system_clock::now(); + std::optional note; + if (context.Args.Contains(Execution::Args::Type::PinNote)) { + note = std::string{ context.Args.GetArg(Execution::Args::Type::PinNote) }; + } + + for (auto& pin : pinsToAddOrUpdate) + { + pin.SetDateAdded(pinTime); + pin.SetNote(note); pinningData.AddOrUpdatePin(pin); } @@ -335,4 +346,106 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << Resource::String::PinNoPinsExist << std::endl; } } + + void ShowPinDetails(Execution::Context& context) + { + auto& pinningData = context.Get(); + auto allPins = pinningData.GetAllPins(); + + // Apply filtering based on provided arguments + bool hasId = context.Args.Contains(Execution::Args::Type::Id); + bool hasName = context.Args.Contains(Execution::Args::Type::Name); + bool hasQuery = context.Args.Contains(Execution::Args::Type::Query); + bool exactMatch = context.Args.Contains(Execution::Args::Type::Exact); + + std::vector matchingPins; + for (const auto& pin : allPins) + { + const auto& packageId = pin.GetKey().PackageId; + + if (hasId) + { + std::string_view idArg = context.Args.GetArg(Execution::Args::Type::Id); + bool match = exactMatch + ? Utility::CaseInsensitiveEquals(packageId, idArg) + : Utility::CaseInsensitiveContainsSubstring(packageId, idArg); + if (!match) + { + continue; + } + } + else if (hasName || hasQuery) + { + // Without an open source, we can only match against PackageId + std::string_view queryArg = hasName + ? context.Args.GetArg(Execution::Args::Type::Name) + : context.Args.GetArg(Execution::Args::Type::Query); + bool match = exactMatch + ? Utility::CaseInsensitiveEquals(packageId, queryArg) + : Utility::CaseInsensitiveContainsSubstring(packageId, queryArg); + if (!match) + { + continue; + } + } + + matchingPins.push_back(pin); + } + + if (matchingPins.empty()) + { + context.Reporter.Info() << Resource::String::PinShowNoMatchFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST); + } + + auto info = context.Reporter.Info(); + bool firstPin = true; + for (const auto& pin : matchingPins) + { + if (!firstPin) + { + info << std::endl; + } + firstPin = false; + + const auto& pinKey = pin.GetKey(); + + // ID + ShowSingleLineField(info, Resource::String::PinShowLabelId, Utility::LocIndView{ pinKey.PackageId }); + + // Source + if (!pinKey.SourceId.empty() && !pinKey.IsForInstalled()) + { + ShowSingleLineField(info, Resource::String::PinShowLabelSource, Utility::LocIndView{ pinKey.SourceId }); + } + + // Type + std::string pinTypeStr{ ToString(pin.GetType()) }; + ShowSingleLineField(info, Resource::String::PinShowLabelType, Utility::LocIndView{ pinTypeStr }); + + // Version (gated version string; empty for pinning/blocking pins) + std::string gatedVersionStr = pin.GetGatedVersion().ToString(); + if (!gatedVersionStr.empty()) + { + ShowSingleLineField(info, Resource::String::PinShowLabelVersion, Utility::LocIndView{ gatedVersionStr }); + } + + // Date Added + const auto& dateAdded = pin.GetDateAdded(); + if (dateAdded.has_value()) + { + std::string dateAddedStr = Utility::TimePointToString(*dateAdded, + Utility::TimeFacet::Year | Utility::TimeFacet::Month | Utility::TimeFacet::Day | + Utility::TimeFacet::Hour | Utility::TimeFacet::Minute | Utility::TimeFacet::Second); + ShowSingleLineField(info, Resource::String::PinShowLabelDateAdded, Utility::LocIndView{ dateAddedStr }); + } + + // Note (only shown if present) + const auto& note = pin.GetNote(); + if (note.has_value() && !note->empty()) + { + ShowSingleLineField(info, Resource::String::PinShowLabelNote, Utility::LocIndView{ *note }); + } + } + } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/PinFlow.h b/src/AppInstallerCLICore/Workflows/PinFlow.h index 2e0e55a5db..e9ff60faa4 100644 --- a/src/AppInstallerCLICore/Workflows/PinFlow.h +++ b/src/AppInstallerCLICore/Workflows/PinFlow.h @@ -54,6 +54,14 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void ReportPins(Execution::Context& context); + // Shows details for a single matching pin (for `winget pin show`). + // Filters the pinning index by query/name/id/exact args and outputs + // detailed field-by-field info (Name, ID, Version, Source, Type, Date Added, Note). + // Required Args: None (at least one of --query/--name/--id expected) + // Inputs: PinningIndex + // Outputs: None + void ShowPinDetails(Execution::Context& context); + // Resets all the existing pins. // Required Args: None // Inputs: None diff --git a/src/AppInstallerCLIPackage/Package.appxmanifest b/src/AppInstallerCLIPackage/Package.appxmanifest index 8559da43ae..0fac2aad64 100644 --- a/src/AppInstallerCLIPackage/Package.appxmanifest +++ b/src/AppInstallerCLIPackage/Package.appxmanifest @@ -86,6 +86,8 @@ + + diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 177d2828c0..a9c0f437a7 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1648,6 +1648,10 @@ Please specify one of them using the --source option to proceed. Pin a specific installed version + + Optional note to store with the pin + Description for the --note argument used with `winget pin add` + Installed Value used in a table to indicate that a package comes from the list of packages installed in the machine @@ -1913,6 +1917,50 @@ Please specify one of them using the --source option to proceed. Pinned version Table header for the version to which a package is pinned; meaning it should not update from that version. + + Time Pinned + Label shown in the pin show output for when the pin was added or last updated. + + + Note + Label shown in the pin show output for the user-provided note stored with the pin. + + + Show details about a pin + Short description of the 'winget pin show' subcommand, shown in help and usage text. + + + Show detailed information about a specific pin, including the package identifier, version, type, date added, and any note stored with the pin. + Long description of the 'winget pin show' subcommand, shown in detailed help text. + + + Date added: + Label shown in the `winget pin show` output for when the pin was added or last updated. + + + Id: + Label shown in the `winget pin show` output for the package identifier. + + + Note: + Label shown in the `winget pin show` output for the user-provided note stored with the pin. + + + Source: + Label shown in the `winget pin show` output for the package source. + + + Pin type: + Label shown in the `winget pin show` output for the type of pin (e.g., Pinning, Gating, Blocking). + + + Pinned version: + Label shown in the `winget pin show` output for the version string of a gating pin. + + + No pin found matching the specified criteria. + Shown when `winget pin show` finds no matching pin. + <See the log file for additional details> The brackets are intended to make the value stand out from other text which it will follow. Any locale appropriate mechanism that achieves this is acceptable. diff --git a/src/AppInstallerCLITests/PinFlow.cpp b/src/AppInstallerCLITests/PinFlow.cpp index ade8c5b252..baaaf7752a 100644 --- a/src/AppInstallerCLITests/PinFlow.cpp +++ b/src/AppInstallerCLITests/PinFlow.cpp @@ -3,6 +3,8 @@ #include "pch.h" #include "WorkflowCommon.h" #include "TestHooks.h" +#include "PinTestCommon.h" +#include #include #include #include @@ -14,7 +16,6 @@ using namespace TestCommon; using namespace AppInstaller::CLI; using namespace AppInstaller::CLI::Workflow; using namespace AppInstaller::Repository::Microsoft; -using namespace AppInstaller::Utility; using namespace AppInstaller::Pinning; using namespace AppInstaller::SQLite; @@ -42,6 +43,8 @@ TEST_CASE("PinFlow_Add", "[PinFlow][workflow]") REQUIRE(pins[0].GetGatedVersion().ToString() == ""); REQUIRE(pins[0].GetKey().PackageId == "AppInstallerCliTest.TestExeInstaller"); REQUIRE(pins[0].GetKey().SourceId == "*TestSource"); + REQUIRE(pins[0].GetDateAdded().has_value()); + REQUIRE_FALSE(pins[0].GetNote().has_value()); std::ostringstream pinListOutput; TestContext listContext{ pinListOutput, std::cin }; @@ -198,4 +201,180 @@ TEST_CASE("PinFlow_ResetEmpty", "[PinFlow][workflow]") INFO(pinResetOutput.str()); REQUIRE(pinResetOutput.str().find(Resource::LocString(Resource::String::PinNoPinsExist)) != std::string::npos); -} \ No newline at end of file +} + +TEST_CASE("PinFlow_Add_WithNote", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + std::ostringstream pinAddOutput; + TestContext addContext{ pinAddOutput, std::cin }; + OverrideForCompositeInstalledSource(addContext, CreateTestSource({ TSR::TestInstaller_Exe })); + addContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe.Query); + addContext.Args.AddArg(Execution::Args::Type::PinNote, "my test note"sv); + + PinAddCommand pinAdd({}); + pinAdd.Execute(addContext); + INFO(pinAddOutput.str()); + + auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read); + auto pins = index.GetAllPins(); + REQUIRE(pins.size() == 1); + REQUIRE(pins[0].GetNote().has_value()); + REQUIRE(pins[0].GetNote().value() == "my test note"); +} + +// Helper: Creates a v1.1 pinning index at the given path and adds the provided pins directly. +// Each pin should already have date_added and note set as desired. +namespace +{ + void PopulatePinIndexForShow(const std::filesystem::path& indexPath, const std::vector& pins) + { + PinningIndex index = PinningIndex::CreateNew(indexPath.u8string(), AppInstaller::SQLite::Version::Latest()); + for (const auto& pin : pins) + { + index.AddPin(pin); + } + } +} + +TEST_CASE("PinFlow_Show_NoMatch", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + Pin existingPin = Pin::CreateBlockingPin({ "SomePackage.Id", "sourceId" }); + existingPin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1000)); + PopulatePinIndexForShow(indexFile.GetPath(), { existingPin }); + + std::ostringstream showOutput; + TestContext showContext{ showOutput, std::cin }; + showContext.Args.AddArg(Execution::Args::Type::Query, "ThisQueryMatchesNothing"sv); + + PinShowCommand pinShow({}); + pinShow.Execute(showContext); + INFO(showOutput.str()); + + REQUIRE_TERMINATED_WITH(showContext, APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST); + REQUIRE(showOutput.str().find(Resource::LocString(Resource::String::PinShowNoMatchFound)) != std::string::npos); +} + +TEST_CASE("PinFlow_Show_MatchById", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + Pin pin = Pin::CreateBlockingPin({ "MyApp.Package", "sourceId" }); + pin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jun2026_01_0900)); + pin.SetNote(std::string{ "keep this one" }); + PopulatePinIndexForShow(indexFile.GetPath(), { pin }); + + std::ostringstream showOutput; + TestContext showContext{ showOutput, std::cin }; + showContext.Args.AddArg(Execution::Args::Type::Id, "MyApp.Package"sv); + + PinShowCommand pinShow({}); + pinShow.Execute(showContext); + INFO(showOutput.str()); + + REQUIRE_FALSE(showContext.IsTerminated()); + REQUIRE(showOutput.str().find("MyApp.Package") != std::string::npos); + REQUIRE(showOutput.str().find("Blocking") != std::string::npos); + REQUIRE(showOutput.str().find("Date added:") != std::string::npos); + REQUIRE(showOutput.str().find("keep this one") != std::string::npos); +} + +TEST_CASE("PinFlow_Show_MatchByQuery", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + Pin pin = Pin::CreatePinningPin({ "Contoso.AppOne", "sourceId" }); + pin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Mar2026_10_1200)); + PopulatePinIndexForShow(indexFile.GetPath(), { pin }); + + std::ostringstream showOutput; + TestContext showContext{ showOutput, std::cin }; + // Partial, case-insensitive match on the package ID + showContext.Args.AddArg(Execution::Args::Type::Query, "appone"sv); + + PinShowCommand pinShow({}); + pinShow.Execute(showContext); + INFO(showOutput.str()); + + REQUIRE_FALSE(showContext.IsTerminated()); + REQUIRE(showOutput.str().find("Contoso.AppOne") != std::string::npos); +} + +TEST_CASE("PinFlow_Show_ExactMatch", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + // Two pins sharing a prefix + Pin pinA = Pin::CreateBlockingPin({ "Vendor.App", "src" }); + pinA.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_01_0000)); + + Pin pinB = Pin::CreateBlockingPin({ "Vendor.AppExtra", "src" }); + pinB.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_01_0000)); + + PopulatePinIndexForShow(indexFile.GetPath(), { pinA, pinB }); + + std::ostringstream showOutput; + TestContext showContext{ showOutput, std::cin }; + showContext.Args.AddArg(Execution::Args::Type::Id, "Vendor.App"sv); + showContext.Args.AddArg(Execution::Args::Type::Exact); + + PinShowCommand pinShow({}); + pinShow.Execute(showContext); + INFO(showOutput.str()); + + REQUIRE_FALSE(showContext.IsTerminated()); + // Only the exact-match pin should appear + REQUIRE(showOutput.str().find("Vendor.App") != std::string::npos); + // The inexact match should NOT appear + REQUIRE(showOutput.str().find("Vendor.AppExtra") == std::string::npos); +} + +TEST_CASE("PinFlow_Show_NoNote_DoesNotShowNoteLabel", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + Pin pin = Pin::CreatePinningPin({ "NoNote.Package", "src" }); + pin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::May2026_01_0800)); + // note intentionally not set + PopulatePinIndexForShow(indexFile.GetPath(), { pin }); + + std::ostringstream showOutput; + TestContext showContext{ showOutput, std::cin }; + showContext.Args.AddArg(Execution::Args::Type::Query, "NoNote.Package"sv); + + PinShowCommand pinShow({}); + pinShow.Execute(showContext); + INFO(showOutput.str()); + + REQUIRE_FALSE(showContext.IsTerminated()); + REQUIRE(showOutput.str().find("NoNote.Package") != std::string::npos); + REQUIRE(showOutput.str().find(Resource::LocString(Resource::String::PinShowLabelNote)) == std::string::npos); +} + +TEST_CASE("PinFlow_Show_EmptyIndex_NoMatch", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + // Create an empty index (no pins) + { PinningIndex::CreateNew(indexFile.GetPath().u8string(), AppInstaller::SQLite::Version::Latest()); } + + std::ostringstream showOutput; + TestContext showContext{ showOutput, std::cin }; + showContext.Args.AddArg(Execution::Args::Type::Query, "AnyQuery"sv); + + PinShowCommand pinShow({}); + pinShow.Execute(showContext); + INFO(showOutput.str()); + + REQUIRE_TERMINATED_WITH(showContext, APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST); +} diff --git a/src/AppInstallerCLITests/PinTestCommon.h b/src/AppInstallerCLITests/PinTestCommon.h new file mode 100644 index 0000000000..982737dc3b --- /dev/null +++ b/src/AppInstallerCLITests/PinTestCommon.h @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +// Named UTC epoch constants (seconds since Unix epoch) shared across pinning tests. +// Use with Utility::ConvertUnixEpochToSystemClock to obtain a time_point. +namespace PinTestEpoch +{ + constexpr int64_t Jan2026_01_0000 = 1767225600LL; // 2026-01-01 00:00:00 UTC + constexpr int64_t Jan2026_15_1000 = 1768471200LL; // 2026-01-15 10:00:00 UTC + constexpr int64_t Jan2026_15_1030 = 1768473000LL; // 2026-01-15 10:30:00 UTC + constexpr int64_t Jan2026_15_1100 = 1768474800LL; // 2026-01-15 11:00:00 UTC + constexpr int64_t Mar2026_10_1200 = 1773144000LL; // 2026-03-10 12:00:00 UTC + constexpr int64_t May2026_01_0800 = 1777622400LL; // 2026-05-01 08:00:00 UTC + constexpr int64_t Jun2026_01_0900 = 1780304400LL; // 2026-06-01 09:00:00 UTC +} diff --git a/src/AppInstallerCLITests/PinningIndex.cpp b/src/AppInstallerCLITests/PinningIndex.cpp index 0682dc31f6..51f2738ef2 100644 --- a/src/AppInstallerCLITests/PinningIndex.cpp +++ b/src/AppInstallerCLITests/PinningIndex.cpp @@ -2,9 +2,12 @@ // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" +#include "PinTestCommon.h" +#include #include #include #include +#include #include using namespace std::string_literals; @@ -163,4 +166,151 @@ TEST_CASE("PinningIndex_AddDuplicatePin", "[pinningIndex]") index.AddPin(pin); REQUIRE_THROWS(index.AddPin(pin), ERROR_ALREADY_EXISTS); -} \ No newline at end of file +} + +TEST_CASE("PinningIndexCreateLatest_HasCorrectVersion", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + PinningIndex index = PinningIndex::CreateNew(tempFile, Version::Latest()); + REQUIRE(index.GetVersion() == Version{ 1, 1 }); +} + +TEST_CASE("PinningIndex_V1_1_AddPin_WithDateAndNote", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Pin pin = Pin::CreateBlockingPin({ "pkgId", "sourceId" }); + pin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1030)); + pin.SetNote(std::string{ "test note" }); + + { + PinningIndex index = PinningIndex::CreateNew(tempFile, Version::Latest()); + index.AddPin(pin); + } + + { + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadOnly); + + auto pinFromIndex = Pinning_V1_1::PinTable::GetPinById(connection, 1); + REQUIRE(pinFromIndex.has_value()); + REQUIRE(pinFromIndex->GetType() == PinType::Blocking); + REQUIRE(pinFromIndex->GetKey().PackageId == "pkgId"); + REQUIRE(pinFromIndex->GetDateAdded() == AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1030)); + REQUIRE(pinFromIndex->GetNote().has_value()); + REQUIRE(pinFromIndex->GetNote().value() == "test note"); + } +} + + +TEST_CASE("PinningIndex_V1_1_AddUpdateRemove", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Pin pin = Pin::CreateBlockingPin({ "pkgId", "srcId" }); + pin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1000)); + pin.SetNote(std::string{ "original note" }); + + Pin updatedPin = Pin::CreateGatingPin({ "pkgId", "srcId" }, { "1.0.*"sv }); + updatedPin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1100)); + updatedPin.SetNote(std::string{ "updated note" }); + + { + PinningIndex index = PinningIndex::CreateNew(tempFile, Version::Latest()); + index.AddPin(pin); + REQUIRE(index.UpdatePin(updatedPin)); + } + + { + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadOnly); + + auto pinFromIndex = Pinning_V1_1::PinTable::GetPinById(connection, 1); + REQUIRE(pinFromIndex.has_value()); + REQUIRE(pinFromIndex->GetType() == PinType::Gating); + REQUIRE(pinFromIndex->GetDateAdded() == AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1100)); + REQUIRE(pinFromIndex->GetNote().has_value()); + REQUIRE(pinFromIndex->GetNote().value() == "updated note"); + } + + { + PinningIndex index = PinningIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + index.RemovePin(updatedPin.GetKey()); + } + + { + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadWrite); + REQUIRE(Pinning_V1_1::PinTable::GetAllPins(connection).empty()); + REQUIRE(!Pinning_V1_1::PinTable::GetPinById(connection, 1)); + } +} + +TEST_CASE("PinningIndex_MigrateFrom_1_0_to_1_1", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Pin pin1 = Pin::CreateBlockingPin({ "pkg1", "src1" }); + Pin pin2 = Pin::CreateGatingPin({ "pkg2", "src2" }, { "2.*"sv }); + + // Create a v1.0 index and add two pins (no date_added or note columns yet) + { + PinningIndex index = PinningIndex::CreateNew(tempFile, { 1, 0 }); + index.AddPin(pin1); + index.AddPin(pin2); + REQUIRE(index.GetVersion() == Version{ 1, 0 }); + } + + // Re-open with ReadWrite: should trigger automatic migration to v1.1 + { + PinningIndex migratedIndex = PinningIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + REQUIRE(migratedIndex.GetVersion() == Version{ 1, 1 }); + + auto pins = migratedIndex.GetAllPins(); + REQUIRE(pins.size() == 2); + + for (const auto& pin : pins) + { + // Migration adds nullable INTEGER and NULL respectively; existing rows get NULL for both + REQUIRE_FALSE(pin.GetDateAdded().has_value()); + REQUIRE_FALSE(pin.GetNote().has_value()); + } + + // Verify that the original pin data is still intact + auto foundPin1 = migratedIndex.GetPin(pin1.GetKey()); + REQUIRE(foundPin1.has_value()); + REQUIRE(foundPin1->GetType() == PinType::Blocking); + + auto foundPin2 = migratedIndex.GetPin(pin2.GetKey()); + REQUIRE(foundPin2.has_value()); + REQUIRE(foundPin2->GetType() == PinType::Gating); + REQUIRE(foundPin2->GetGatedVersion().ToString() == pin2.GetGatedVersion().ToString()); + } +} + +TEST_CASE("PinningIndex_MigrateFrom_1_0_to_1_1_ReadOnly_Uses_OldInterface", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Pin pin = Pin::CreatePinningPin({ "pkgId", "srcId" }); + + // Create a v1.0 index and add a pin + { + PinningIndex index = PinningIndex::CreateNew(tempFile, { 1, 0 }); + index.AddPin(pin); + } + + // Re-open with Read (read-only): should NOT migrate, should use v1.0 interface + { + PinningIndex readOnlyIndex = PinningIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::Read); + REQUIRE(readOnlyIndex.GetVersion() == Version{ 1, 0 }); + + auto pins = readOnlyIndex.GetAllPins(); + REQUIRE(pins.size() == 1); + REQUIRE(pins[0].GetType() == PinType::Pinning); + REQUIRE(pins[0].GetKey() == pin.GetKey()); + } +} diff --git a/src/AppInstallerCLITests/SQLiteWrapper.cpp b/src/AppInstallerCLITests/SQLiteWrapper.cpp index 7fa70aa955..01f9affc82 100644 --- a/src/AppInstallerCLITests/SQLiteWrapper.cpp +++ b/src/AppInstallerCLITests/SQLiteWrapper.cpp @@ -54,7 +54,7 @@ void InsertIntoSimpleTestTable(Connection& connection, int firstVal, const std:: void UpdateSimpleTestTable(Connection& connection, int firstVal, const std::string& secondVal) { Builder::StatementBuilder update; - update.Update(s_tableName).Set().Column(s_firstColumn).Equals(firstVal).Column(s_secondColumn).Equals(secondVal); + update.Update(s_tableName).Set().Column(s_firstColumn).AssignValue(firstVal).Column(s_secondColumn).AssignValue(secondVal); update.Execute(connection); } diff --git a/src/AppInstallerCommonCore/Public/winget/Pin.h b/src/AppInstallerCommonCore/Public/winget/Pin.h index 2039b81a5e..247b9d7176 100644 --- a/src/AppInstallerCommonCore/Public/winget/Pin.h +++ b/src/AppInstallerCommonCore/Public/winget/Pin.h @@ -3,6 +3,9 @@ #pragma once #include "winget/Manifest.h" #include "AppInstallerVersions.h" +#include +#include +#include namespace AppInstaller::Pinning { @@ -97,6 +100,11 @@ namespace AppInstaller::Pinning PinType GetType() const { return m_type; } const PinKey& GetKey() const { return m_key; } const Utility::GatedVersion& GetGatedVersion() const { return m_gatedVersion; } + const std::optional& GetDateAdded() const { return m_dateAdded; } + const std::optional& GetNote() const { return m_note; } + + void SetDateAdded(std::optional dateAdded) { m_dateAdded = std::move(dateAdded); } + void SetNote(std::optional note) { m_note = std::move(note); } bool operator==(const Pin& other) const; bool operator<(const Pin& other) const @@ -114,5 +122,7 @@ namespace AppInstaller::Pinning PinType m_type = PinType::Unknown; PinKey m_key; Utility::GatedVersion m_gatedVersion; + std::optional m_dateAdded; + std::optional m_note; }; } diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj index 53a8260f9c..f18955f27e 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -338,6 +338,8 @@ + + @@ -449,6 +451,8 @@ + + diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters index d877dedeab..897c8ab613 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -82,6 +82,9 @@ {f05e19bb-2161-4ab0-9d04-2dfa2d3eb3c6} + + {a3e2f7c1-5d84-4b9e-8f02-1c6d3a7b0e45} + {21da32f5-b918-436e-96a9-465525f90259} @@ -375,6 +378,12 @@ Microsoft\Schema\Pinning_1_0 + + Microsoft\Schema\Pinning_1_1 + + + Microsoft\Schema\Pinning_1_1 + Public\winget @@ -695,6 +704,9 @@ Microsoft\Schema\Pinning_1_0 + + Microsoft\Schema\Pinning_1_1 + Source Files @@ -728,6 +740,9 @@ Microsoft\Schema\Pinning_1_0 + + Microsoft\Schema\Pinning_1_1 + Rest\Schema\1_7 diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp index 645cfb127d..bb03125e7c 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp @@ -4,6 +4,7 @@ #include "PinningIndex.h" #include #include "Schema/Pinning_1_0/PinningIndexInterface.h" +#include "Schema/Pinning_1_1/PinningIndexInterface.h" namespace AppInstaller::Repository::Microsoft { @@ -184,15 +185,20 @@ namespace AppInstaller::Repository::Microsoft return m_interface->ResetAllPins(m_dbconn, sourceId); } - std::unique_ptr PinningIndex::CreateIPinningIndex() const + std::unique_ptr PinningIndex::CreateIPinningIndex(const SQLite::Version& version) { - if (m_version == SQLite::Version{ 1, 0 } || - m_version.MajorVersion == 1 || - m_version.IsLatest()) + if (version == SQLite::Version{ 1, 0 }) { return std::make_unique(); } + if (version == SQLite::Version{ 1, 1 } || + version.MajorVersion == 1 || + version.IsLatest()) + { + return std::make_unique(); + } + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } @@ -200,13 +206,44 @@ namespace AppInstaller::Repository::Microsoft SQLiteStorageBase(target, disposition, std::move(indexFile)) { AICLI_LOG(Repo, Info, << "Opened Pinning Index with version [" << m_version << "], last write [" << GetLastWriteTime() << "]"); - m_interface = CreateIPinningIndex(); - THROW_HR_IF(APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX, disposition == SQLiteStorageBase::OpenDisposition::ReadWrite && m_version != m_interface->GetVersion()); + + // Create the correct interface for the stored schema version. + m_interface = CreateIPinningIndex(m_version); + + if (disposition == SQLiteStorageBase::OpenDisposition::ReadWrite) + { + // For writable opens, create a latest interface and migrate if the stored version is older. + auto latestInterface = CreateIPinningIndex(SQLite::Version::Latest()); + + if (m_version != latestInterface->GetVersion()) + { + AICLI_LOG(Repo, Info, << "Attempting to migrate Pinning Index from [" << m_version << "] to [" << latestInterface->GetVersion() << "]"); + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "pinningindex_migrate"); + bool migrated = latestInterface->MigrateFrom(m_dbconn, m_interface.get()); + + if (migrated) + { + latestInterface->GetVersion().SetSchemaVersion(m_dbconn); + SetLastWriteTime(); + savepoint.Commit(); + m_version = latestInterface->GetVersion(); + AICLI_LOG(Repo, Info, << "Migration successful"); + } + else + { + AICLI_LOG(Repo, Error, << "Migration failed"); + THROW_HR(APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX); + } + } + + m_interface = std::move(latestInterface); + } } PinningIndex::PinningIndex(const std::string& target, SQLite::Version version) : SQLiteStorageBase(target, version) { - m_interface = CreateIPinningIndex(); + m_interface = CreateIPinningIndex(version); m_version = m_interface->GetVersion(); } } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h index 672f8dba8f..62a92a17e0 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h @@ -67,8 +67,8 @@ namespace AppInstaller::Repository::Microsoft // Constructor used to create a new index. PinningIndex(const std::string& target, SQLite::Version version); - // Creates the IPinningIndex interface object for this version. - std::unique_ptr CreateIPinningIndex() const; + // Creates an IPinningIndex interface object for a specific version. + static std::unique_ptr CreateIPinningIndex(const SQLite::Version& version); std::unique_ptr m_interface; }; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.cpp index 2301d0d4b3..ed88f44f17 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.cpp @@ -133,7 +133,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 if (tableValue.value() != value) { SQLite::Builder::StatementBuilder updateBuilder; - updateBuilder.Update(tableName).Set().Column(valueName).Equals(value).Where(SQLite::RowIDName).Equals(selectResult); + updateBuilder.Update(tableName).Set().Column(valueName).AssignValue(value).Where(SQLite::RowIDName).Equals(selectResult); updateBuilder.Execute(connection); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp index dd34ecbc57..d4fd017d64 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp @@ -135,7 +135,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 { // Reset all filter values to unselected SQLite::Builder::StatementBuilder builder; - builder.Update(GetQualifiedName()).Set().Column(s_SearchResultsTable_Filter).Equals(false); + builder.Update(GetQualifiedName()).Set().Column(s_SearchResultsTable_Filter).AssignValue(false); builder.Execute(m_connection); } @@ -153,7 +153,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // ) // ) StatementBuilder builder; - builder.Update(GetQualifiedName()).Set().Column(s_SearchResultsTable_Filter).Equals(true).Where(s_SearchResultsTable_Manifest).In().BeginParenthetical(). + builder.Update(GetQualifiedName()).Set().Column(s_SearchResultsTable_Filter).AssignValue(true).Where(s_SearchResultsTable_Manifest).In().BeginParenthetical(). Select(s_SearchResultsTable_SubSelect_ManifestAlias).From().BeginParenthetical(); // Add the field specific portion diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/ManifestMetadataTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/ManifestMetadataTable.cpp index dd461b5f41..dd52c5e8c8 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/ManifestMetadataTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/ManifestMetadataTable.cpp @@ -103,7 +103,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_1 // UPSERT (aka ON CONFLICT) is not available to us, as it was only introduced in 3.24.0 (2018-06-04), // and we need to support Windows 10 (17763) which was released in 2017. StatementBuilder updateBuilder; - updateBuilder.Update(s_ManifestMetadataTable_Table_Name).Set().Column(s_ManifestMetadataTable_Value_Column).Equals(value). + updateBuilder.Update(s_ManifestMetadataTable_Table_Name).Set().Column(s_ManifestMetadataTable_Value_Column).AssignValue(value). Where(s_ManifestMetadataTable_Manifest_Column).Equals(manifestId).And(s_ManifestMetadataTable_Metadata_Column).Equals(metadata); updateBuilder.Execute(connection); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/OneToManyTableWithMap.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/OneToManyTableWithMap.cpp index c0b2b75dde..8f0678427e 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/OneToManyTableWithMap.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/OneToManyTableWithMap.cpp @@ -134,7 +134,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 if (tableValue.value() != value) { SQLite::Builder::StatementBuilder updateBuilder; - updateBuilder.Update(tableName).Set().Column(valueName).Equals(value).Where(SQLite::RowIDName).Equals(selectResult); + updateBuilder.Update(tableName).Set().Column(valueName).AssignValue(value).Where(SQLite::RowIDName).Equals(selectResult); updateBuilder.Execute(connection); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp index 1728a5d6e7..fceea986ad 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp @@ -123,9 +123,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // First attempt to update the row and then insert it if no modification occurred. Builder::StatementBuilder updateBuilder; updateBuilder.Update(s_PUTT_Table_Name).Set(). - Column(s_PUTT_WriteTime).Equals(currentTime). - Column(s_PUTT_Manifest).Equals(compressedManifest). - Column(s_PUTT_Hash).Equals(manifestHash). + Column(s_PUTT_WriteTime).AssignValue(currentTime). + Column(s_PUTT_Manifest).AssignValue(compressedManifest). + Column(s_PUTT_Hash).AssignValue(manifestHash). Where(s_PUTT_Package).LikeWithEscape(packageIdentifier); updateBuilder.Execute(connection); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/SearchResultsTable_2_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/SearchResultsTable_2_0.cpp index 52dea8d7f0..735a34f9e9 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/SearchResultsTable_2_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/SearchResultsTable_2_0.cpp @@ -137,7 +137,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { // Reset all filter values to unselected SQLite::Builder::StatementBuilder builder; - builder.Update(GetQualifiedName()).Set().Column(s_SearchResultsTable_Filter).Equals(false); + builder.Update(GetQualifiedName()).Set().Column(s_SearchResultsTable_Filter).AssignValue(false); builder.Execute(m_connection); } @@ -155,7 +155,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // ) // ) StatementBuilder builder; - builder.Update(GetQualifiedName()).Set().Column(s_SearchResultsTable_Filter).Equals(true).Where(s_SearchResultsTable_Package).In().BeginParenthetical(). + builder.Update(GetQualifiedName()).Set().Column(s_SearchResultsTable_Filter).AssignValue(true).Where(s_SearchResultsTable_Package).In().BeginParenthetical(). Select(s_SearchResultsTable_SubSelect_PackageAlias).From().BeginParenthetical(); // Add the field specific portion diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/IPinningIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPinningIndex.h index 8eba0be3d6..9fde5e84a1 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/IPinningIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPinningIndex.h @@ -17,6 +17,10 @@ namespace AppInstaller::Repository::Microsoft::Schema // Creates all of the version dependent tables within the database. virtual void CreateTables(SQLite::Connection& connection) = 0; + // Migrates the schema from an older version. + // Returns true if migration succeeded, false if migration is not applicable. + virtual bool MigrateFrom(SQLite::Connection& connection, const IPinningIndex* current) = 0; + // Version 1.0 // Adds a pin to the index. virtual SQLite::rowid_t AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) = 0; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp index 370c3f1e29..da89a2e1d7 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp @@ -113,10 +113,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 SQLite::Builder::StatementBuilder builder; const auto& pinKey = pin.GetKey(); builder.Update(s_PinTable_Table_Name).Set() - .Column(s_PinTable_PackageId_Column).Equals((std::string_view)pinKey.PackageId) - .Column(s_PinTable_SourceId_Column).Equals(pinKey.SourceId) - .Column(s_PinTable_Type_Column).Equals(pin.GetType()) - .Column(s_PinTable_Version_Column).Equals(pin.GetGatedVersion().ToString()) + .Column(s_PinTable_PackageId_Column).AssignValue((std::string_view)pinKey.PackageId) + .Column(s_PinTable_SourceId_Column).AssignValue(pinKey.SourceId) + .Column(s_PinTable_Type_Column).AssignValue(pin.GetType()) + .Column(s_PinTable_Version_Column).AssignValue(pin.GetGatedVersion().ToString()) .Where(SQLite::RowIDName).Equals(pinId); builder.Execute(connection); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.h index 4bfafd616a..8deb47e6d1 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.h @@ -13,9 +13,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 // Get the table name. static std::string_view TableName(); - // Creates the table with named indices. - static void Create(SQLite::Connection& connection); - // Gets the row ID for the pin, if it exists. static std::optional GetIdByPinKey(SQLite::Connection& connection, const Pinning::PinKey& pinKey); @@ -39,5 +36,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 // Resets all pins from a given source, or from all sources if none is specified. // Returns a value indicating whether there were any changes. static bool ResetAllPins(SQLite::Connection& connection, std::string_view sourceId = {}); + + protected: + // Creates the table with named indices. + static void Create(SQLite::Connection& connection); + + friend struct PinningIndexInterface; }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h index 7d7a38f0dc..8cc664e60c 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h @@ -10,6 +10,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 // Version 1.0 SQLite::Version GetVersion() const override; void CreateTables(SQLite::Connection& connection) override; + bool MigrateFrom(SQLite::Connection& connection, const IPinningIndex* current) override; private: SQLite::rowid_t AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) override; @@ -18,5 +19,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 std::optional GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) override; std::vector GetAllPins(SQLite::Connection& connection) override; bool ResetAllPins(SQLite::Connection& connection, std::string_view sourceId) override; + + protected: + virtual SQLite::rowid_t IAddPin(SQLite::Connection& connection, const Pinning::Pin& pin); + virtual bool IUpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin); + virtual std::optional IGetPinById(SQLite::Connection& connection, SQLite::rowid_t pinId); + virtual std::vector IGetAllPins(SQLite::Connection& connection); }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp index 2d81684846..38a95fcbc3 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp @@ -35,6 +35,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 savepoint.Commit(); } + bool PinningIndexInterface::MigrateFrom(SQLite::Connection&, const IPinningIndex*) + { + // Version 1.0 cannot migrate from any prior version. + return false; + } + SQLite::rowid_t PinningIndexInterface::AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) { auto existingPin = GetExistingPinId(connection, pin.GetKey()); @@ -42,7 +48,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), existingPin.has_value()); SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "addpin_v1_0"); - SQLite::rowid_t pinId = PinTable::AddPin(connection, pin); + SQLite::rowid_t pinId = IAddPin(connection, pin); savepoint.Commit(); return pinId; @@ -57,7 +63,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "updatepin_v1_0"); - bool status = PinTable::UpdatePinById(connection, existingPinId.value(), pin); + bool status = IUpdatePinById(connection, existingPinId.value(), pin); savepoint.Commit(); return { status, existingPinId.value() }; @@ -87,12 +93,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 return {}; } - return PinTable::GetPinById(connection, existingPinId.value()); + return IGetPinById(connection, existingPinId.value()); } std::vector PinningIndexInterface::GetAllPins(SQLite::Connection& connection) { - return PinTable::GetAllPins(connection); + return IGetAllPins(connection); } bool PinningIndexInterface::ResetAllPins(SQLite::Connection& connection, std::string_view sourceId) @@ -103,4 +109,24 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 return result; } + + SQLite::rowid_t PinningIndexInterface::IAddPin(SQLite::Connection& connection, const Pinning::Pin& pin) + { + return PinTable::AddPin(connection, pin); + } + + bool PinningIndexInterface::IUpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin) + { + return PinTable::UpdatePinById(connection, pinId, pin); + } + + std::optional PinningIndexInterface::IGetPinById(SQLite::Connection& connection, SQLite::rowid_t pinId) + { + return PinTable::GetPinById(connection, pinId); + } + + std::vector PinningIndexInterface::IGetAllPins(SQLite::Connection& connection) + { + return PinTable::GetAllPins(connection); + } } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable_1_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable_1_1.cpp new file mode 100644 index 0000000000..77ba57d6fd --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable_1_1.cpp @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "PinTable_1_1.h" +#include +#include +#include "Microsoft/Schema/IPinningIndex.h" + +namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 +{ + namespace + { + using PinRow = std::tuple, std::optional>; + + std::optional GetPinFromRow(PinRow&& row) + { + auto [packageId, sourceId, type, version, epochOpt, note] = std::move(row); + + std::optional result; + + Pinning::PinKey key; + key.PackageId = std::move(packageId); + key.SourceId = std::move(sourceId); + + switch (type) + { + case Pinning::PinType::Blocking: + result = Pinning::Pin::CreateBlockingPin(std::move(key)); + break; + case Pinning::PinType::Pinning: + result = Pinning::Pin::CreatePinningPin(std::move(key)); + break; + case Pinning::PinType::Gating: + result = Pinning::Pin::CreateGatingPin(std::move(key), Utility::GatedVersion{ std::move(version) }); + break; + default: + return {}; + } + + std::optional dateAdded; + if (epochOpt.has_value()) + { + dateAdded = Utility::ConvertUnixEpochToSystemClock(*epochOpt); + } + + result->SetDateAdded(std::move(dateAdded)); + result->SetNote(std::move(note)); + + return result; + } + } + + using namespace std::string_view_literals; + static constexpr std::string_view s_PinTable_Table_Name = "pin"sv; + static constexpr std::string_view s_PinTable_PackageId_Column = "package_id"sv; + static constexpr std::string_view s_PinTable_SourceId_Column = "source_id"sv; + static constexpr std::string_view s_PinTable_Type_Column = "type"sv; + static constexpr std::string_view s_PinTable_Version_Column = "version"sv; + static constexpr std::string_view s_PinTable_DateAdded_Column = "date_added"sv; + static constexpr std::string_view s_PinTable_Note_Column = "note"sv; + + void PinTable::MigrateFrom1_0(SQLite::Connection& connection) + { + using namespace SQLite::Builder; + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "migratepintable_v1_0_to_v1_1_pintable"); + + StatementBuilder addDateAdded; + addDateAdded.AlterTable(s_PinTable_Table_Name).Add(s_PinTable_DateAdded_Column, Type::Integer); + addDateAdded.Execute(connection); + + StatementBuilder addNote; + addNote.AlterTable(s_PinTable_Table_Name).Add(s_PinTable_Note_Column, Type::Text); + addNote.Execute(connection); + + savepoint.Commit(); + } + + SQLite::rowid_t PinTable::AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) + { + SQLite::Builder::StatementBuilder builder; + const auto& pinKey = pin.GetKey(); + + int64_t epochToStore = pin.GetDateAdded().has_value() + ? Utility::ConvertSystemClockToUnixEpoch(*pin.GetDateAdded()) + : Utility::ConvertSystemClockToUnixEpoch(std::chrono::system_clock::now()); + + builder.InsertInto(s_PinTable_Table_Name) + .Columns({ + s_PinTable_PackageId_Column, + s_PinTable_SourceId_Column, + s_PinTable_Type_Column, + s_PinTable_Version_Column, + s_PinTable_DateAdded_Column, + s_PinTable_Note_Column }) + .Values( + pinKey.PackageId, + pinKey.SourceId, + pin.GetType(), + pin.GetGatedVersion().ToString(), + epochToStore, + pin.GetNote()); + + builder.Execute(connection); + return connection.GetLastInsertRowID(); + } + + bool PinTable::UpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin) + { + SQLite::Builder::StatementBuilder builder; + const auto& pinKey = pin.GetKey(); + + int64_t epochToStore = pin.GetDateAdded().has_value() + ? Utility::ConvertSystemClockToUnixEpoch(*pin.GetDateAdded()) + : Utility::ConvertSystemClockToUnixEpoch(std::chrono::system_clock::now()); + + builder.Update(s_PinTable_Table_Name).Set() + .Column(s_PinTable_PackageId_Column).AssignValue(pinKey.PackageId) + .Column(s_PinTable_SourceId_Column).AssignValue(pinKey.SourceId) + .Column(s_PinTable_Type_Column).AssignValue(pin.GetType()) + .Column(s_PinTable_Version_Column).AssignValue(pin.GetGatedVersion().ToString()) + .Column(s_PinTable_DateAdded_Column).AssignValue(epochToStore) + .Column(s_PinTable_Note_Column).AssignValue(pin.GetNote()); + + builder.Where(SQLite::RowIDName).Equals(pinId); + builder.Execute(connection); + return connection.GetChanges() != 0; + } + + std::optional PinTable::GetPinById(SQLite::Connection& connection, const SQLite::rowid_t pinId) + { + SQLite::Builder::StatementBuilder builder; + builder.Select({ + s_PinTable_PackageId_Column, + s_PinTable_SourceId_Column, + s_PinTable_Type_Column, + s_PinTable_Version_Column, + s_PinTable_DateAdded_Column, + s_PinTable_Note_Column }) + .From(s_PinTable_Table_Name).Where(SQLite::RowIDName).Equals(pinId); + + SQLite::Statement select = builder.Prepare(connection); + + if (!select.Step()) + { + return {}; + } + + return GetPinFromRow(select.GetRow, std::optional>()); + } + + std::vector PinTable::GetAllPins(SQLite::Connection& connection) + { + SQLite::Builder::StatementBuilder builder; + builder.Select({ + s_PinTable_PackageId_Column, + s_PinTable_SourceId_Column, + s_PinTable_Type_Column, + s_PinTable_Version_Column, + s_PinTable_DateAdded_Column, + s_PinTable_Note_Column }) + .From(s_PinTable_Table_Name); + + SQLite::Statement select = builder.Prepare(connection); + + std::vector pins; + while (select.Step()) + { + auto pin = GetPinFromRow(select.GetRow, std::optional>()); + if (pin) + { + pins.push_back(std::move(pin.value())); + } + } + + return pins; + } + +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable_1_1.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable_1_1.h new file mode 100644 index 0000000000..dbd3a56638 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable_1_1.h @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include +#include +#include "Microsoft/Schema/IPinningIndex.h" +#include "Microsoft/Schema/Pinning_1_0/PinTable.h" +#include + +namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 +{ + struct PinTable : Pinning_V1_0::PinTable + { + // Migrates an existing v1.0 pin table by adding the date_added and note columns. + static void MigrateFrom1_0(SQLite::Connection& connection); + + // Adds a new pin. Returns the row ID of the added pin. + static SQLite::rowid_t AddPin(SQLite::Connection& connection, const Pinning::Pin& pin); + + // Updates an existing pin. + // Returns a value indicating whether there were any changes. + static bool UpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin); + + // Gets a pin by its row ID if it exists. + // Used for testing + static std::optional GetPinById(SQLite::Connection& connection, const SQLite::rowid_t pinId); + + // Gets all the currently existing pins. + static std::vector GetAllPins(SQLite::Connection& connection); + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h new file mode 100644 index 0000000000..8c8820127f --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h" + +namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 +{ + struct PinningIndexInterface : public Pinning_V1_0::PinningIndexInterface + { + SQLite::Version GetVersion() const override; + void CreateTables(SQLite::Connection& connection) override; + bool MigrateFrom(SQLite::Connection& connection, const IPinningIndex* current) override; + + protected: + SQLite::rowid_t IAddPin(SQLite::Connection& connection, const Pinning::Pin& pin) override; + bool IUpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin) override; + std::optional IGetPinById(SQLite::Connection& connection, SQLite::rowid_t pinId) override; + std::vector IGetAllPins(SQLite::Connection& connection) override; + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp new file mode 100644 index 0000000000..76fe9e209d --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h" +#include "Microsoft/Schema/Pinning_1_1/PinTable_1_1.h" + +namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 +{ + // Version 1.1 + SQLite::Version PinningIndexInterface::GetVersion() const + { + return { 1, 1 }; + } + + void PinningIndexInterface::CreateTables(SQLite::Connection& connection) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createpintable_v1_1"); + Pinning_V1_0::PinningIndexInterface base; + base.CreateTables(connection); + MigrateFrom(connection, &base); + savepoint.Commit(); + } + + bool PinningIndexInterface::MigrateFrom(SQLite::Connection& connection, const IPinningIndex* current) + { + if (!current || current->GetVersion() != SQLite::Version{ 1, 0 }) + { + return false; + } + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "migratepintable_v1_0_to_v1_1"); + Pinning_V1_1::PinTable::MigrateFrom1_0(connection); + savepoint.Commit(); + + return true; + } + + // Override the pin methods to use the correct PinTable methods for version 1.1 + + SQLite::rowid_t PinningIndexInterface::IAddPin(SQLite::Connection& connection, const Pinning::Pin& pin) + { + return PinTable::AddPin(connection, pin); + } + + bool PinningIndexInterface::IUpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin) + { + return PinTable::UpdatePinById(connection, pinId, pin); + } + + std::optional PinningIndexInterface::IGetPinById(SQLite::Connection& connection, SQLite::rowid_t pinId) + { + return PinTable::GetPinById(connection, pinId); + } + + std::vector PinningIndexInterface::IGetAllPins(SQLite::Connection& connection) + { + return PinTable::GetAllPins(connection); + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp index 7557493f97..bcded14e6e 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp @@ -82,10 +82,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 { SQLite::Builder::StatementBuilder builder; builder.Update(s_PortableTable_Table_Name).Set() - .Column(s_PortableTable_FilePath_Column).Equals(file.GetFilePath().u8string()) - .Column(s_PortableTable_FileType_Column).Equals(file.FileType) - .Column(s_PortableTable_SHA256_Column).Equals(file.SHA256) - .Column(s_PortableTable_SymlinkTarget_Column).Equals(file.SymlinkTarget) + .Column(s_PortableTable_FilePath_Column).AssignValue(file.GetFilePath().u8string()) + .Column(s_PortableTable_FileType_Column).AssignValue(file.FileType) + .Column(s_PortableTable_SHA256_Column).AssignValue(file.SHA256) + .Column(s_PortableTable_SymlinkTarget_Column).AssignValue(file.SymlinkTarget) .Where(SQLite::RowIDName).Equals(id); builder.Execute(connection); diff --git a/src/AppInstallerRepositoryCore/PinningData.cpp b/src/AppInstallerRepositoryCore/PinningData.cpp index ac29bb0d0a..801f4f5fe1 100644 --- a/src/AppInstallerRepositoryCore/PinningData.cpp +++ b/src/AppInstallerRepositoryCore/PinningData.cpp @@ -93,6 +93,17 @@ namespace AppInstaller::Pinning m_database->RemovePin(pinKey); } + bool PinningData::TryRemovePin(const PinKey& pinKey) + { + THROW_HR_IF(E_NOT_VALID_STATE, !IsDatabaseConnected()); + if (!m_database->GetPin(pinKey)) + { + return false; + } + m_database->RemovePin(pinKey); + return true; + } + std::optional PinningData::GetPin(const PinKey& pinKey) { return IsDatabaseConnected() ? m_database->GetPin(pinKey) : std::nullopt; diff --git a/src/AppInstallerRepositoryCore/Public/winget/PinningData.h b/src/AppInstallerRepositoryCore/Public/winget/PinningData.h index ca1e3b36ce..d754611593 100644 --- a/src/AppInstallerRepositoryCore/Public/winget/PinningData.h +++ b/src/AppInstallerRepositoryCore/Public/winget/PinningData.h @@ -58,6 +58,8 @@ namespace AppInstaller::Pinning // Pass through functions to the index itself void AddOrUpdatePin(const Pin& pin); void RemovePin(const PinKey& pinKey); + // Removes the pin if it exists. Returns true if a pin was removed, false if none was found. + bool TryRemovePin(const PinKey& pinKey); std::optional GetPin(const PinKey& pinKey); std::vector GetAllPins(); bool ResetAllPins(std::string_view sourceId = {}); diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h index 7de19998d7..0452f4f5d0 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h @@ -278,6 +278,34 @@ namespace AppInstaller::SQLite::Builder return IsNull(); } } + + // Assigns a value using "= ?" binding semantics. When the optional is empty, binds NULL + // via the "= ?" parameter rather than producing "IS NULL". Use this instead of + // Equals(optional) in UPDATE SET clauses, where "IS NULL" is invalid SQL. + template + StatementBuilder& AssignValue(const std::optional& value) + { + if (value) + { + AddBindFunctor(AppendOpAndBinder(Op::Equals), value.value()); + } + else + { + AddBindFunctor(AppendOpAndBinder(Op::Equals), nullptr); + } + return *this; + } + + // Assigns a non-nullable value using "= ?" binding semantics. Prefer this over Equals() + // in UPDATE SET clauses to clearly signal assignment intent and prevent future breakage + // if the type is later made optional. + template + StatementBuilder& AssignValue(const ValueType& value) + { + AddBindFunctor(AppendOpAndBinder(Op::Equals), value); + return *this; + } + // The optional index value can be used to specify the parameter index. StatementBuilder& Equals(details::unbound_t, std::optional index = {}); StatementBuilder& Equals(std::nullptr_t); diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp b/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp index e2003aa68f..669826fa86 100644 --- a/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp +++ b/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp @@ -169,12 +169,12 @@ namespace winrt::Microsoft::Management::Configuration::implementation::Database: StatementBuilder builder; builder.Update(s_SetInfoTable_Table).Set(). - Column(s_SetInfoTable_Column_Name).Equals(ConvertToUTF8(configurationSet.Name())). - Column(s_SetInfoTable_Column_Origin).Equals(ConvertToUTF8(configurationSet.Origin())). - Column(s_SetInfoTable_Column_Path).Equals(ConvertToUTF8(configurationSet.Path())). - Column(s_SetInfoTable_Column_SchemaVersion).Equals(ConvertToUTF8(schemaVersion)). - Column(s_SetInfoTable_Column_Metadata).Equals(serializer->SerializeMetadataWithEnvironment(configurationSet.Metadata(), configurationSet.Environment())). - Column(s_SetInfoTable_Column_Variables).Equals(serializer->SerializeValueSet(configurationSet.Variables())). + Column(s_SetInfoTable_Column_Name).AssignValue(ConvertToUTF8(configurationSet.Name())). + Column(s_SetInfoTable_Column_Origin).AssignValue(ConvertToUTF8(configurationSet.Origin())). + Column(s_SetInfoTable_Column_Path).AssignValue(ConvertToUTF8(configurationSet.Path())). + Column(s_SetInfoTable_Column_SchemaVersion).AssignValue(ConvertToUTF8(schemaVersion)). + Column(s_SetInfoTable_Column_Metadata).AssignValue(serializer->SerializeMetadataWithEnvironment(configurationSet.Metadata(), configurationSet.Environment())). + Column(s_SetInfoTable_Column_Variables).AssignValue(serializer->SerializeValueSet(configurationSet.Variables())). Where(RowIDName).Equals(target); builder.Execute(m_connection); diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_2/QueueTable.cpp b/src/Microsoft.Management.Configuration/Database/Schema/0_2/QueueTable.cpp index adcd41ccc4..7920aa8fc5 100644 --- a/src/Microsoft.Management.Configuration/Database/Schema/0_2/QueueTable.cpp +++ b/src/Microsoft.Management.Configuration/Database/Schema/0_2/QueueTable.cpp @@ -95,7 +95,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation::Database: void QueueTable::SetActiveQueueItem(const std::string& objectName) { StatementBuilder builder; - builder.Update(s_QueueTable_Table).Set().Column(s_QueueTable_Column_Active).Equals(true).Where(s_QueueTable_Column_ObjectName).Equals(objectName); + builder.Update(s_QueueTable_Table).Set().Column(s_QueueTable_Column_Active).AssignValue(true).Where(s_QueueTable_Column_ObjectName).Equals(objectName); builder.Execute(m_connection); } diff --git a/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs b/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs index ecae3141cf..02c7d6109f 100644 --- a/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs +++ b/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs @@ -152,16 +152,28 @@ internal static class ClassesDefinition } }, - [typeof(EditPackageCatalogOptions)] = new() - { - ProjectedClassType = typeof(EditPackageCatalogOptions), - InterfaceType = typeof(IEditPackageCatalogOptions), - Clsids = new Dictionary() - { - [ClsidContext.InProc] = new Guid("E8E12FE1-AB77-40C4-A562-E91FB51B4E82"), - [ClsidContext.OutOfProc] = new Guid("A9F5E736-68CE-463C-BA6D-DE968F0CCE04"), - [ClsidContext.OutOfProcDev] = new Guid("29B19238-81AD-4A8E-A2FC-ADF17C38CAEB"), - } + [typeof(EditPackageCatalogOptions)] = new() + { + ProjectedClassType = typeof(EditPackageCatalogOptions), + InterfaceType = typeof(IEditPackageCatalogOptions), + Clsids = new Dictionary() + { + [ClsidContext.InProc] = new Guid("E8E12FE1-AB77-40C4-A562-E91FB51B4E82"), + [ClsidContext.OutOfProc] = new Guid("A9F5E736-68CE-463C-BA6D-DE968F0CCE04"), + [ClsidContext.OutOfProcDev] = new Guid("29B19238-81AD-4A8E-A2FC-ADF17C38CAEB"), + } + }, + + [typeof(PinPackageOptions)] = new() + { + ProjectedClassType = typeof(PinPackageOptions), + InterfaceType = typeof(IPinPackageOptions), + Clsids = new Dictionary() + { + [ClsidContext.InProc] = new Guid("BA9BB1AF-4453-4274-BBF9-4C17794EFA8E"), + [ClsidContext.OutOfProc] = new Guid("93409EF2-29D0-46D3-8085-13EDE73939C4"), + [ClsidContext.OutOfProcDev] = new Guid("B3A61CCB-A3D0-497D-B300-A904904EEA56"), + } } }; diff --git a/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs b/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs index 356d1ff8b3..84f305b210 100644 --- a/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs +++ b/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs @@ -36,11 +36,13 @@ public WinGetProjectionFactory(IInstanceInitializer instanceInitializer) public PackageManagerSettings CreatePackageManagerSettings() => InstanceInitializer.CreateInstance(); public RepairOptions CreateRepairOptions() => InstanceInitializer.CreateInstance(); - - public AddPackageCatalogOptions CreateAddPackageCatalogOptions() => InstanceInitializer.CreateInstance(); - - public RemovePackageCatalogOptions CreateRemovePackageCatalogOptions() => InstanceInitializer.CreateInstance(); - + + public AddPackageCatalogOptions CreateAddPackageCatalogOptions() => InstanceInitializer.CreateInstance(); + + public RemovePackageCatalogOptions CreateRemovePackageCatalogOptions() => InstanceInitializer.CreateInstance(); + public EditPackageCatalogOptions CreateEditPackageCatalogOptions() => InstanceInitializer.CreateInstance(); + + public PinPackageOptions CreatePinPackageOptions() => InstanceInitializer.CreateInstance(); } } diff --git a/src/Microsoft.Management.Deployment/ComClsids.cpp b/src/Microsoft.Management.Deployment/ComClsids.cpp index 24c64ec942..daf023da93 100644 --- a/src/Microsoft.Management.Deployment/ComClsids.cpp +++ b/src/Microsoft.Management.Deployment/ComClsids.cpp @@ -1,64 +1,65 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "Public/ComClsids.h" -#pragma warning( push ) -#pragma warning ( disable : 4467 ) -// 4467 Allow use of uuid attribute for com object creation. -#include "PackageManager.h" -#include "FindPackagesOptions.h" -#include "CreateCompositePackageCatalogOptions.h" -#include "InstallOptions.h" -#include "UninstallOptions.h" -#include "PackageMatchFilter.h" -#include "PackageManagerSettings.h" -#include "DownloadOptions.h" +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Public/ComClsids.h" +#pragma warning( push ) +#pragma warning ( disable : 4467 ) +// 4467 Allow use of uuid attribute for com object creation. +#include "PackageManager.h" +#include "FindPackagesOptions.h" +#include "CreateCompositePackageCatalogOptions.h" +#include "InstallOptions.h" +#include "UninstallOptions.h" +#include "PackageMatchFilter.h" +#include "PackageManagerSettings.h" +#include "DownloadOptions.h" #include "AuthenticationArguments.h" #include "RepairOptions.h" #include "AddPackageCatalogOptions.h" -#include "RemovePackageCatalogOptions.h" -#include "EditPackageCatalogOptions.h" -#pragma warning( pop ) - -namespace winrt::Microsoft::Management::Deployment -{ - CLSID GetRedirectedClsidFromInProcClsid(REFCLSID clsid) - { - if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PackageManager)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PackageManager); - } - else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_FindPackagesOptions)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::FindPackagesOptions); - } - else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_CreateCompositePackageCatalogOptions)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::CreateCompositePackageCatalogOptions); - } - else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_InstallOptions)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::InstallOptions); - } - else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_UninstallOptions)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::UninstallOptions); - } - else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_DownloadOptions)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::DownloadOptions); - } - else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PackageMatchFilter)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PackageMatchFilter); - } - else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_AuthenticationArguments)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::AuthenticationArguments); - } - else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PackageManagerSettings)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PackageManagerSettings); +#include "RemovePackageCatalogOptions.h" +#include "EditPackageCatalogOptions.h" +#include "PinPackageOptions.h" +#pragma warning( pop ) + +namespace winrt::Microsoft::Management::Deployment +{ + CLSID GetRedirectedClsidFromInProcClsid(REFCLSID clsid) + { + if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PackageManager)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PackageManager); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_FindPackagesOptions)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::FindPackagesOptions); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_CreateCompositePackageCatalogOptions)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::CreateCompositePackageCatalogOptions); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_InstallOptions)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::InstallOptions); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_UninstallOptions)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::UninstallOptions); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_DownloadOptions)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::DownloadOptions); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PackageMatchFilter)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PackageMatchFilter); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_AuthenticationArguments)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::AuthenticationArguments); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PackageManagerSettings)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PackageManagerSettings); } else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_RepairOptions)) { @@ -75,10 +76,14 @@ namespace winrt::Microsoft::Management::Deployment else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_EditPackageCatalogOptions)) { return __uuidof(winrt::Microsoft::Management::Deployment::implementation::EditPackageCatalogOptions); - } - else - { - return CLSID_NULL; - } - } + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PinPackageOptions)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PinPackageOptions); + } + else + { + return CLSID_NULL; + } + } } diff --git a/src/Microsoft.Management.Deployment/Converters.cpp b/src/Microsoft.Management.Deployment/Converters.cpp index 3c3b4f238b..da6ae41e91 100644 --- a/src/Microsoft.Management.Deployment/Converters.cpp +++ b/src/Microsoft.Management.Deployment/Converters.cpp @@ -543,6 +543,29 @@ namespace winrt::Microsoft::Management::Deployment::implementation } } + PinResultStatus GetPinOperationStatus(winrt::hresult hresult) + { + switch (hresult) + { + case S_OK: + return PinResultStatus::Ok; + case APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY: + case APPINSTALLER_CLI_ERROR_MSSTORE_BLOCKED_BY_POLICY: + case APPINSTALLER_CLI_ERROR_MSSTORE_APP_BLOCKED_BY_POLICY: + case APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED: + return PinResultStatus::BlockedByPolicy; + case APPINSTALLER_CLI_ERROR_PIN_ALREADY_EXISTS: + return PinResultStatus::PackagePinAlreadyExists; + case APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST: + return PinResultStatus::PackagePinNotFound; + case E_INVALIDARG: + case APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS: + return PinResultStatus::InvalidOptions; + default: + return PinResultStatus::InternalError; + } + } + ::AppInstaller::Manifest::PlatformEnum GetPlatformEnum(WindowsPlatform value) { switch (value) @@ -556,4 +579,21 @@ namespace winrt::Microsoft::Management::Deployment::implementation default: return AppInstaller::Manifest::PlatformEnum::Unknown; } } + + winrt::Microsoft::Management::Deployment::PackagePinType ConvertPinType(::AppInstaller::Pinning::PinType type) + { + switch (type) + { + case ::AppInstaller::Pinning::PinType::PinnedByManifest: + return winrt::Microsoft::Management::Deployment::PackagePinType::PinnedByManifest; + case ::AppInstaller::Pinning::PinType::Pinning: + return winrt::Microsoft::Management::Deployment::PackagePinType::Pinning; + case ::AppInstaller::Pinning::PinType::Gating: + return winrt::Microsoft::Management::Deployment::PackagePinType::Gating; + case ::AppInstaller::Pinning::PinType::Blocking: + return winrt::Microsoft::Management::Deployment::PackagePinType::Blocking; + default: + return winrt::Microsoft::Management::Deployment::PackagePinType::Unknown; + } + } } diff --git a/src/Microsoft.Management.Deployment/Converters.h b/src/Microsoft.Management.Deployment/Converters.h index 643658705c..903a77e61e 100644 --- a/src/Microsoft.Management.Deployment/Converters.h +++ b/src/Microsoft.Management.Deployment/Converters.h @@ -3,6 +3,7 @@ #pragma once #include "PackageMatchFilter.g.h" #include +#include #include #include #include @@ -34,6 +35,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::AddPackageCatalogStatus GetAddPackageCatalogOperationStatus(winrt::hresult hresult); winrt::Microsoft::Management::Deployment::RemovePackageCatalogStatus GetRemovePackageCatalogOperationStatus(winrt::hresult hresult); winrt::Microsoft::Management::Deployment::EditPackageCatalogStatus GetEditPackageCatalogOperationStatus(winrt::hresult hresult); + winrt::Microsoft::Management::Deployment::PinResultStatus GetPinOperationStatus(winrt::hresult hresult); + winrt::Microsoft::Management::Deployment::PackagePinType ConvertPinType(::AppInstaller::Pinning::PinType type); ::AppInstaller::Manifest::PlatformEnum GetPlatformEnum(winrt::Microsoft::Management::Deployment::WindowsPlatform value); #define WINGET_GET_OPERATION_RESULT_STATUS(_installResultStatus_, _uninstallResultStatus_, _downloadResultStatus_, _repairResultStatus_) \ diff --git a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj index 86dbab6088..89f91e9222 100644 --- a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj +++ b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj @@ -171,6 +171,9 @@ + + + @@ -223,6 +226,9 @@ + + + diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index 39d53771b5..50d83853a3 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -20,6 +20,9 @@ #include // 4467 Allow use of uuid attribute for com object creation. #include "PackageManager.h" +#include "PackagePin.h" +#include "PinPackageOptions.h" +#include "PinPackageResult.h" #pragma warning( pop ) #include "PackageManager.g.cpp" #include "CatalogPackage.h" @@ -40,6 +43,10 @@ #include "AppInstallerRuntime.h" #include #include +#include +#include +#include +#include using namespace std::literals::chrono_literals; using namespace ::AppInstaller::CLI; @@ -1488,5 +1495,257 @@ namespace winrt::Microsoft::Management::Deployment::implementation return GetEditPackageCatalogResult(terminationHR); } + namespace + { + // Builds PackagePin WinRT objects from a vector of internal Pin objects. + winrt::Windows::Foundation::Collections::IVectorView + MakePackagePinVectorView(const std::vector<::AppInstaller::Pinning::Pin>& pins) + { + auto result = winrt::single_threaded_vector(); + for (const auto& pin : pins) + { + auto packagePin = winrt::make_self>(); + packagePin->Initialize(pin); + result.Append(*packagePin); + } + return result.GetView(); + } + + // Collects PinKeys for all available sources of a package, and optionally for its installed identity. + std::vector<::AppInstaller::Pinning::PinKey> GetPinKeysForCatalogPackage( + winrt::Microsoft::Management::Deployment::CatalogPackage const& package, + bool includeInstalled) + { + std::vector<::AppInstaller::Pinning::PinKey> pinKeys; + + // Keys for each available source version + for (auto const& versionId : package.AvailableVersions()) + { + auto versionInfo = package.GetPackageVersionInfo(versionId); + if (versionInfo) + { + std::string packageId = winrt::to_string(versionInfo.Id()); + std::string sourceId = winrt::to_string(versionInfo.PackageCatalog().Info().Id()); + if (!packageId.empty() && !sourceId.empty()) + { + ::AppInstaller::Pinning::PinKey key{ packageId, sourceId }; + // Avoid duplicates (same packageId+sourceId may appear for multiple versions) + if (std::find(pinKeys.begin(), pinKeys.end(), key) == pinKeys.end()) + { + pinKeys.push_back(std::move(key)); + } + } + } + } + + // Key for the installed package identity (ProductCode / PackageFamilyName) + if (includeInstalled) + { + auto installedVersion = package.InstalledVersion(); + if (installedVersion) + { + // Prefer PackageFamilyName (MSIX), fall back to ProductCode (MSI/EXE) + auto pfns = installedVersion.PackageFamilyNames(); + if (pfns && pfns.Size() > 0) + { + for (auto const& pfn : pfns) + { + pinKeys.push_back(::AppInstaller::Pinning::PinKey::GetPinKeyForInstalled(winrt::to_string(pfn))); + } + } + else + { + auto productCodes = installedVersion.ProductCodes(); + if (productCodes) + { + for (auto const& productCode : productCodes) + { + pinKeys.push_back(::AppInstaller::Pinning::PinKey::GetPinKeyForInstalled(winrt::to_string(productCode))); + } + } + } + } + } + + return pinKeys; + } + + winrt::Microsoft::Management::Deployment::PinPackageResult MakePinPackageResult(HRESULT hr) + { + auto result = winrt::make_self>(); + result->Initialize(GetPinOperationStatus(hr), hr); + return *result; + } + + // Converts PinPackageOptions.PinType to the internal pin representation. + ::AppInstaller::Pinning::Pin CreatePinFromOptions( + const ::AppInstaller::Pinning::PinKey& pinKey, + winrt::Microsoft::Management::Deployment::PinPackageOptions const& options) + { + switch (options.PinType()) + { + case winrt::Microsoft::Management::Deployment::PackagePinType::Pinning: + return ::AppInstaller::Pinning::Pin::CreatePinningPin(pinKey); + case winrt::Microsoft::Management::Deployment::PackagePinType::Blocking: + return ::AppInstaller::Pinning::Pin::CreateBlockingPin(pinKey); + case winrt::Microsoft::Management::Deployment::PackagePinType::Gating: + return ::AppInstaller::Pinning::Pin::CreateGatingPin( + pinKey, + ::AppInstaller::Utility::GatedVersion{ winrt::to_string(options.GatedVersion()) }); + default: + // Unknown, PinnedByManifest, and any future unrecognized values are not valid + // user-settable pin types. + THROW_HR(E_INVALIDARG); + } + } + } + + winrt::Windows::Foundation::Collections::IVectorView + PackageManager::GetAllPins() + { + LogStartupIfApplicable(); + + THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageQuery)); + + auto pinningData = ::AppInstaller::Pinning::PinningData{ ::AppInstaller::Pinning::PinningData::Disposition::ReadOnly }; + return MakePackagePinVectorView(pinningData.GetAllPins()); + } + + winrt::Windows::Foundation::Collections::IVectorView + PackageManager::GetPins(winrt::Microsoft::Management::Deployment::CatalogPackage package) + { + LogStartupIfApplicable(); + + THROW_HR_IF_NULL(E_POINTER, package); + THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageQuery)); + + auto pinKeys = GetPinKeysForCatalogPackage(package, /* includeInstalled */ true); + auto pinningData = ::AppInstaller::Pinning::PinningData{ ::AppInstaller::Pinning::PinningData::Disposition::ReadOnly }; + + std::vector<::AppInstaller::Pinning::Pin> pins; + for (const auto& pinKey : pinKeys) + { + auto pin = pinningData.GetPin(pinKey); + if (pin) + { + pins.push_back(std::move(*pin)); + } + } + + return MakePackagePinVectorView(pins); + } + + winrt::Microsoft::Management::Deployment::PinPackageResult PackageManager::PinPackage( + winrt::Microsoft::Management::Deployment::CatalogPackage package, + winrt::Microsoft::Management::Deployment::PinPackageOptions options) + { + LogStartupIfApplicable(); + + THROW_HR_IF_NULL(E_POINTER, package); + THROW_HR_IF_NULL(E_POINTER, options); + THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageManagement)); + + // Gating pins require a non-empty version range. + THROW_HR_IF(E_INVALIDARG, + options.PinType() == winrt::Microsoft::Management::Deployment::PackagePinType::Gating && + options.GatedVersion().empty()); + + HRESULT terminationHR = S_OK; + try + { + auto pinKeys = GetPinKeysForCatalogPackage(package, options.PinInstalledPackage()); + THROW_HR_IF(E_INVALIDARG, pinKeys.empty()); + + auto pinningData = ::AppInstaller::Pinning::PinningData{ ::AppInstaller::Pinning::PinningData::Disposition::ReadWrite }; + + auto pinTime = std::chrono::system_clock::now(); + + std::optional note; + if (!options.Note().empty()) + { + note = winrt::to_string(options.Note()); + } + + for (const auto& pinKey : pinKeys) + { + auto newPin = CreatePinFromOptions(pinKey, options); + + auto existingPin = pinningData.GetPin(pinKey); + if (existingPin && !(*existingPin == newPin)) + { + THROW_HR_IF(APPINSTALLER_CLI_ERROR_PIN_ALREADY_EXISTS, !options.Force()); + } + + newPin.SetDateAdded(pinTime); + newPin.SetNote(note); + pinningData.AddOrUpdatePin(newPin); + } + } + catch (...) + { + terminationHR = AppInstaller::CLI::Workflow::HandleException(nullptr, std::current_exception()); + } + + return MakePinPackageResult(terminationHR); + } + + winrt::Microsoft::Management::Deployment::PinPackageResult PackageManager::UnpinPackage( + winrt::Microsoft::Management::Deployment::CatalogPackage package) + { + LogStartupIfApplicable(); + + THROW_HR_IF_NULL(E_POINTER, package); + THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageManagement)); + + HRESULT terminationHR = S_OK; + try + { + auto pinKeys = GetPinKeysForCatalogPackage(package, /* includeInstalled */ true); + auto pinningData = ::AppInstaller::Pinning::PinningData{ ::AppInstaller::Pinning::PinningData::Disposition::ReadWrite }; + + bool anyRemoved = false; + for (const auto& pinKey : pinKeys) + { + anyRemoved |= pinningData.TryRemovePin(pinKey); + } + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST, !anyRemoved); + } + catch (...) + { + terminationHR = AppInstaller::CLI::Workflow::HandleException(nullptr, std::current_exception()); + } + + return MakePinPackageResult(terminationHR); + } + + winrt::Microsoft::Management::Deployment::PinPackageResult PackageManager::ResetAllPins(winrt::Microsoft::Management::Deployment::PackageCatalogReference packageCatalogReference) + { + LogStartupIfApplicable(); + + THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageManagement)); + + HRESULT terminationHR = S_OK; + try + { + std::string sourceId; + if (packageCatalogReference) + { + sourceId = winrt::to_string(packageCatalogReference.Info().Id()); + } + + auto pinningData = ::AppInstaller::Pinning::PinningData{ ::AppInstaller::Pinning::PinningData::Disposition::ReadWrite }; + pinningData.ResetAllPins(sourceId); + } + catch (...) + { + terminationHR = AppInstaller::CLI::Workflow::HandleException(nullptr, std::current_exception()); + } + + return MakePinPackageResult(terminationHR); + } + CoCreatableMicrosoftManagementDeploymentClass(PackageManager); } diff --git a/src/Microsoft.Management.Deployment/PackageManager.h b/src/Microsoft.Management.Deployment/PackageManager.h index c973df2dbe..a61057fc28 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.h +++ b/src/Microsoft.Management.Deployment/PackageManager.h @@ -54,6 +54,12 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::hstring Version() const; // Contract 28.0 winrt::Microsoft::Management::Deployment::EditPackageCatalogResult EditPackageCatalog(winrt::Microsoft::Management::Deployment::EditPackageCatalogOptions options); + // Contract 29.0 + winrt::Windows::Foundation::Collections::IVectorView GetAllPins(); + winrt::Windows::Foundation::Collections::IVectorView GetPins(winrt::Microsoft::Management::Deployment::CatalogPackage package); + winrt::Microsoft::Management::Deployment::PinPackageResult PinPackage(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::PinPackageOptions options); + winrt::Microsoft::Management::Deployment::PinPackageResult UnpinPackage(winrt::Microsoft::Management::Deployment::CatalogPackage package); + winrt::Microsoft::Management::Deployment::PinPackageResult ResetAllPins(winrt::Microsoft::Management::Deployment::PackageCatalogReference packageCatalogReference); }; #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl index 25567032f2..6a6340b9a4 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.idl +++ b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -1679,8 +1679,130 @@ namespace Microsoft.Management.Deployment /// Edit an existing Windows Package Catalog. EditPackageCatalogResult EditPackageCatalog(EditPackageCatalogOptions options); } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] + { + /// Get all pins across all sources. + Windows.Foundation.Collections.IVectorView GetAllPins(); + + /// Get the pins associated with the specified package. + /// Returns pins for all sources the package is available from, and for the installed + /// package identity if InstalledVersion is present. + Windows.Foundation.Collections.IVectorView GetPins(CatalogPackage package); + + /// Add or update a pin for the specified package. + /// Requires Force = true to overwrite an existing pin of a different type. + PinPackageResult PinPackage(CatalogPackage package, PinPackageOptions options); + + /// Remove all pins associated with the specified package. + /// Returns PackagePinNotFound if no pin exists for the package. + PinPackageResult UnpinPackage(CatalogPackage package); + + /// Reset (remove) all pins. If packageCatalogReference is non-null, only pins from that + /// catalog are removed; otherwise all pins are removed. + PinPackageResult ResetAllPins(PackageCatalogReference packageCatalogReference); + } } + /// IMPLEMENTATION NOTE: Pinning::PinType from AppInstaller::Pinning + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] + enum PackagePinType + { + /// Unknown pin type or not pinned. + Unknown, + /// Pinned by the manifest using the RequiresExplicitUpgrade field. + /// Behaves the same as Pinning. + PinnedByManifest, + /// The package is excluded from upgrade --all, unless --include-pinned is added. + /// upgrade is not blocked. + Pinning, + /// The package is pinned to a specific version range. + Gating, + /// The package is blocked from upgrade --all and upgrade . + /// User must unpin to allow update. + Blocking, + }; + + /// IMPLEMENTATION NOTE: Pinning::Pin from AppInstaller::Pinning + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] + runtimeclass PackagePin + { + /// The package ID that the pin applies to (for available-package pins) or the + /// ProductCode/PackageFamilyName (for installed-package pins). + String PackageId { get; }; + + /// The source identifier the pin applies to. Empty for installed-package pins. + String SourceId { get; }; + + /// The type of the pin. + PackagePinType Type { get; }; + + /// The gated version range. Only meaningful when Type is Gating; empty otherwise. + String GatedVersion { get; }; + + /// The UTC date/time when the pin was added. Null if not set. + Windows.Foundation.IReference DateAdded { get; }; + + /// Optional note associated with the pin. May be empty. + String Note { get; }; + + /// True if this pin is keyed on the installed package identity + /// (ProductCode or PackageFamilyName) rather than available package ID + source. + Boolean IsForInstalledPackage { get; }; + }; + + /// Options for adding or updating a package pin. + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] + runtimeclass PinPackageOptions + { + PinPackageOptions(); + + /// The type of pin to create. Required. + PackagePinType PinType; + + /// The gated version range. Required when PinType is Gating; ignored otherwise. + /// SAMPLE VALUE: "1.0.*" or "<2.0" + String GatedVersion; + + /// When true, the pin is keyed on the installed package identity (ProductCode or + /// PackageFamilyName) instead of the available package ID and source. + /// Mirrors the winget pin add --installed flag. Default is false. + Boolean PinInstalledPackage; + + /// When true, an existing pin with a different type will be overwritten. + /// When false and a pin already exists with a different type, the operation returns + /// PinResultStatus::PackagePinAlreadyExists. Default is false. + Boolean Force; + + /// Optional note to store with the pin. + String Note; + }; + + /// Status of a pin operation. + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] + enum PinResultStatus + { + Ok, + BlockedByPolicy, + InternalError, + InvalidOptions, + /// Returned by UnpinPackage when no pin exists for the package. + PackagePinNotFound, + /// Returned by PinPackage when a pin with a different type already exists + /// and Force is false. + PackagePinAlreadyExists, + }; + + /// Result of a pin operation. + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] + runtimeclass PinPackageResult + { + PinResultStatus Status { get; }; + + /// The error code of the overall operation. + HRESULT ExtendedErrorCode { get; }; + }; + /// Global settings for PackageManager operations. /// This settings should be invoked prior to invocation of PackageManager class. /// This settings is only exposed in in-proc Com invocation. @@ -1789,5 +1911,7 @@ namespace Microsoft.Management.Deployment interface Windows.Foundation.Collections.IVectorView; interface Windows.Foundation.Collections.IVector; interface Windows.Foundation.Collections.IVectorView; + interface Windows.Foundation.Collections.IVector; + interface Windows.Foundation.Collections.IVectorView; } } diff --git a/src/Microsoft.Management.Deployment/PackagePin.cpp b/src/Microsoft.Management.Deployment/PackagePin.cpp new file mode 100644 index 0000000000..d0b649da89 --- /dev/null +++ b/src/Microsoft.Management.Deployment/PackagePin.cpp @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "PackagePin.h" +#include "PackagePin.g.cpp" +#include "Converters.h" +#include + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + void PackagePin::Initialize(const ::AppInstaller::Pinning::Pin& pin) + { + m_packageId = winrt::to_hstring(pin.GetKey().PackageId); + m_sourceId = winrt::to_hstring(pin.GetKey().SourceId); + m_type = ConvertPinType(pin.GetType()); + m_gatedVersion = winrt::to_hstring(pin.GetGatedVersion().ToString()); + const auto& dateAdded = pin.GetDateAdded(); + if (dateAdded.has_value()) + { + m_dateAdded = winrt::clock::from_sys(*dateAdded); + } + m_note = pin.GetNote() ? winrt::to_hstring(*pin.GetNote()) : hstring{}; + m_isForInstalledPackage = pin.GetKey().IsForInstalled(); + } + + hstring PackagePin::PackageId() + { + return m_packageId; + } + + hstring PackagePin::SourceId() + { + return m_sourceId; + } + + winrt::Microsoft::Management::Deployment::PackagePinType PackagePin::Type() + { + return m_type; + } + + hstring PackagePin::GatedVersion() + { + return m_gatedVersion; + } + + winrt::Windows::Foundation::IReference PackagePin::DateAdded() + { + return m_dateAdded; + } + + hstring PackagePin::Note() + { + return m_note; + } + + bool PackagePin::IsForInstalledPackage() + { + return m_isForInstalledPackage; + } +} diff --git a/src/Microsoft.Management.Deployment/PackagePin.h b/src/Microsoft.Management.Deployment/PackagePin.h new file mode 100644 index 0000000000..384170843e --- /dev/null +++ b/src/Microsoft.Management.Deployment/PackagePin.h @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "PackagePin.g.h" +#include + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + struct PackagePin : PackagePinT + { + PackagePin() = default; + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + void Initialize(const ::AppInstaller::Pinning::Pin& pin); +#endif + + hstring PackageId(); + hstring SourceId(); + winrt::Microsoft::Management::Deployment::PackagePinType Type(); + hstring GatedVersion(); + winrt::Windows::Foundation::IReference DateAdded(); + hstring Note(); + bool IsForInstalledPackage(); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + hstring m_packageId; + hstring m_sourceId; + winrt::Microsoft::Management::Deployment::PackagePinType m_type = winrt::Microsoft::Management::Deployment::PackagePinType::Unknown; + hstring m_gatedVersion; + winrt::Windows::Foundation::IReference m_dateAdded{ nullptr }; + hstring m_note; + bool m_isForInstalledPackage = false; +#endif + }; +} diff --git a/src/Microsoft.Management.Deployment/PinPackageOptions.cpp b/src/Microsoft.Management.Deployment/PinPackageOptions.cpp new file mode 100644 index 0000000000..f7e4359c8e --- /dev/null +++ b/src/Microsoft.Management.Deployment/PinPackageOptions.cpp @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#pragma warning( push ) +#pragma warning ( disable : 4467 6388) +// 6388 Allow CreateInstance. +#include +// 4467 Allow use of uuid attribute for com object creation. +#include "PinPackageOptions.h" +#pragma warning( pop ) +#include "PinPackageOptions.g.cpp" +#include "Helpers.h" + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + winrt::Microsoft::Management::Deployment::PackagePinType PinPackageOptions::PinType() + { + return m_pinType; + } + + void PinPackageOptions::PinType(winrt::Microsoft::Management::Deployment::PackagePinType value) + { + m_pinType = value; + } + + hstring PinPackageOptions::GatedVersion() + { + return m_gatedVersion; + } + + void PinPackageOptions::GatedVersion(hstring const& value) + { + m_gatedVersion = value; + } + + bool PinPackageOptions::PinInstalledPackage() + { + return m_pinInstalledPackage; + } + + void PinPackageOptions::PinInstalledPackage(bool value) + { + m_pinInstalledPackage = value; + } + + bool PinPackageOptions::Force() + { + return m_force; + } + + void PinPackageOptions::Force(bool value) + { + m_force = value; + } + + hstring PinPackageOptions::Note() + { + return m_note; + } + + void PinPackageOptions::Note(hstring const& value) + { + m_note = value; + } + + CoCreatableMicrosoftManagementDeploymentClass(PinPackageOptions); +} diff --git a/src/Microsoft.Management.Deployment/PinPackageOptions.h b/src/Microsoft.Management.Deployment/PinPackageOptions.h new file mode 100644 index 0000000000..8200d32682 --- /dev/null +++ b/src/Microsoft.Management.Deployment/PinPackageOptions.h @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "PinPackageOptions.g.h" +#include "public/ComClsids.h" +#include + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + [uuid(WINGET_OUTOFPROC_COM_CLSID_PinPackageOptions)] + struct PinPackageOptions : PinPackageOptionsT + { + PinPackageOptions() = default; + + winrt::Microsoft::Management::Deployment::PackagePinType PinType(); + void PinType(winrt::Microsoft::Management::Deployment::PackagePinType value); + + hstring GatedVersion(); + void GatedVersion(hstring const& value); + + bool PinInstalledPackage(); + void PinInstalledPackage(bool value); + + bool Force(); + void Force(bool value); + + hstring Note(); + void Note(hstring const& value); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + winrt::Microsoft::Management::Deployment::PackagePinType m_pinType = winrt::Microsoft::Management::Deployment::PackagePinType::Pinning; + hstring m_gatedVersion; + bool m_pinInstalledPackage = false; + bool m_force = false; + hstring m_note; +#endif + }; +} + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) +namespace winrt::Microsoft::Management::Deployment::factory_implementation +{ + struct PinPackageOptions : + PinPackageOptionsT, + AppInstaller::WinRT::ModuleCountBase + { + }; +} +#endif diff --git a/src/Microsoft.Management.Deployment/PinPackageResult.cpp b/src/Microsoft.Management.Deployment/PinPackageResult.cpp new file mode 100644 index 0000000000..76e52f5336 --- /dev/null +++ b/src/Microsoft.Management.Deployment/PinPackageResult.cpp @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "PinPackageResult.h" +#include "PinPackageResult.g.cpp" +#include + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + void PinPackageResult::Initialize( + winrt::Microsoft::Management::Deployment::PinResultStatus status, + winrt::hresult extendedErrorCode) + { + m_status = status; + m_extendedErrorCode = extendedErrorCode; + } + + winrt::Microsoft::Management::Deployment::PinResultStatus PinPackageResult::Status() + { + return m_status; + } + + winrt::hresult PinPackageResult::ExtendedErrorCode() + { + return m_extendedErrorCode; + } +} diff --git a/src/Microsoft.Management.Deployment/PinPackageResult.h b/src/Microsoft.Management.Deployment/PinPackageResult.h new file mode 100644 index 0000000000..f96d1ef343 --- /dev/null +++ b/src/Microsoft.Management.Deployment/PinPackageResult.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "PinPackageResult.g.h" + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + struct PinPackageResult : PinPackageResultT + { + PinPackageResult() = default; + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + void Initialize( + winrt::Microsoft::Management::Deployment::PinResultStatus status, + winrt::hresult extendedErrorCode); +#endif + + winrt::Microsoft::Management::Deployment::PinResultStatus Status(); + winrt::hresult ExtendedErrorCode(); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + winrt::Microsoft::Management::Deployment::PinResultStatus m_status = winrt::Microsoft::Management::Deployment::PinResultStatus::Ok; + winrt::hresult m_extendedErrorCode = S_OK; +#endif + }; +} diff --git a/src/Microsoft.Management.Deployment/Public/ComClsids.h b/src/Microsoft.Management.Deployment/Public/ComClsids.h index e9137d788f..0eede1998a 100644 --- a/src/Microsoft.Management.Deployment/Public/ComClsids.h +++ b/src/Microsoft.Management.Deployment/Public/ComClsids.h @@ -18,6 +18,7 @@ #define WINGET_OUTOFPROC_COM_CLSID_AddPackageCatalogOptions "DB9D012D-00D7-47EE-8FB1-606E10AC4F51" #define WINGET_OUTOFPROC_COM_CLSID_RemovePackageCatalogOptions "032B1C58-B975-469B-A013-E632B6ECE8D8" #define WINGET_OUTOFPROC_COM_CLSID_EditPackageCatalogOptions "A9F5E736-68CE-463C-BA6D-DE968F0CCE04" +#define WINGET_OUTOFPROC_COM_CLSID_PinPackageOptions "93409EF2-29D0-46D3-8085-13EDE73939C4" #else #define WINGET_OUTOFPROC_COM_CLSID_PackageManager "74CB3139-B7C5-4B9E-9388-E6616DEA288C" #define WINGET_OUTOFPROC_COM_CLSID_FindPackagesOptions "1BD8FF3A-EC50-4F69-AEEE-DF4C9D3BAA96" @@ -32,6 +33,7 @@ #define WINGET_OUTOFPROC_COM_CLSID_AddPackageCatalogOptions "D58C7E4C-70E6-476C-A5D4-80341ED80252" #define WINGET_OUTOFPROC_COM_CLSID_RemovePackageCatalogOptions "87A96609-1A39-4955-BE72-7174E147B7DC" #define WINGET_OUTOFPROC_COM_CLSID_EditPackageCatalogOptions "29B19238-81AD-4A8E-A2FC-ADF17C38CAEB" +#define WINGET_OUTOFPROC_COM_CLSID_PinPackageOptions "B3A61CCB-A3D0-497D-B300-A904904EEA56" #endif // Clsids only used in in-proc invocation @@ -53,6 +55,7 @@ namespace winrt::Microsoft::Management::Deployment const CLSID WINGET_INPROC_COM_CLSID_AddPackageCatalogOptions = { 0x24e6f1fa, 0xe4c3, 0x4acd, 0x96, 0x5d, 0xdf, 0x21, 0x3f, 0xd5, 0x8f, 0x15 }; // {24E6F1FA-E4C3-4ACD-965D-DF213FD58F15} const CLSID WINGET_INPROC_COM_CLSID_RemovePackageCatalogOptions = { 0x1125d3a6, 0xe2ce, 0x479a, 0x91, 0xd5, 0x71, 0xa3, 0xf6, 0xf8, 0xb0, 0xb }; // {1125D3A6-E2CE-479A-91D5-71A3F6F8B00B} const CLSID WINGET_INPROC_COM_CLSID_EditPackageCatalogOptions = { 0xe8e12fe1, 0xab77, 0x40c4, 0xa5, 0x62, 0xe9, 0x1f, 0xb5, 0x1b, 0x4e, 0x82 }; // {E8E12FE1-AB77-40C4-A562-E91FB51B4E82} + const CLSID WINGET_INPROC_COM_CLSID_PinPackageOptions = { 0xba9bb1af, 0x4453, 0x4274, 0xbb, 0xf9, 0x4c, 0x17, 0x79, 0x4e, 0xfa, 0x8e }; // {BA9BB1AF-4453-4274-BBF9-4C17794EFA8E} CLSID GetRedirectedClsidFromInProcClsid(REFCLSID clsid); } diff --git a/src/PowerShell/Help/Microsoft.WinGet.Client/Add-WinGetPin.md b/src/PowerShell/Help/Microsoft.WinGet.Client/Add-WinGetPin.md new file mode 100644 index 0000000000..b0bb276916 --- /dev/null +++ b/src/PowerShell/Help/Microsoft.WinGet.Client/Add-WinGetPin.md @@ -0,0 +1,347 @@ +--- +external help file: Microsoft.WinGet.Client.Cmdlets.dll-Help.xml +Module Name: Microsoft.WinGet.Client +ms.date: 08/01/2024 +online version: +schema: 2.0.0 +--- + +# Add-WinGetPin + +## SYNOPSIS +Adds a WinGet package pin. + +## SYNTAX + +### FoundSet (Default) +``` +Add-WinGetPin [-Blocking] [-GatedVersion ] [-PinInstalledPackage] + [-Force] [-Note ] [-Id ] [-Name ] [-Moniker ] [-Source ] + [[-Query] ] [-MatchOption ] [-ProgressAction ] + [-WhatIf] [-Confirm] [] +``` + +### GivenSet +``` +Add-WinGetPin [-Blocking] [-GatedVersion ] [-PinInstalledPackage] + [-Force] [-Note ] [[-PSCatalogPackage] ] + [-ProgressAction ] [-WhatIf] [-Confirm] [] +``` + +## DESCRIPTION +This command adds a pin for a WinGet package, preventing automatic updates. Mirrors the behavior +of `winget pin add`. By default, all string-based searches are case-insensitive exact matches. +Wildcards are not supported. You can change the search behavior using the **MatchOption** parameter. + +Pin types are determined by the parameters you provide: +- **Pinning** (default): Prevents automatic updates but allows manual upgrades. +- **Blocking**: Use `-Blocking` to prevent all upgrades, including manual ones. +- **Gating**: Use `-GatedVersion` to allow upgrades only to versions satisfying the specified range. + +## EXAMPLES + +### Example 1: Pin a package by Id +```powershell +Add-WinGetPin -Id "Microsoft.PowerShell" +``` +This example adds a Pinning pin for `Microsoft.PowerShell`, preventing automatic updates. + +### Example 2: Add a blocking pin +```powershell +Add-WinGetPin -Id "Microsoft.PowerShell" -Blocking +``` +This example adds a Blocking pin for `Microsoft.PowerShell`, preventing all upgrades. + +### Example 3: Add a gating pin +```powershell +Add-WinGetPin -Id "Microsoft.PowerShell" -GatedVersion "<7.5" +``` +This example adds a Gating pin that allows upgrades only to versions below 7.5. + +### Example 4: Pin a package using the pipeline +```powershell +Find-WinGetPackage -Id "Microsoft.PowerShell" | Add-WinGetPin +``` +This example pins a package passed from `Find-WinGetPackage`. + +### Example 5: Pin an installed package +```powershell +Get-WinGetPackage -Id "Microsoft.PowerShell" | Add-WinGetPin -PinInstalledPackage +``` +This example adds a pin for the currently installed version of `Microsoft.PowerShell`. + +## PARAMETERS + +### -Confirm + +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force + +Forces the pin operation even if an existing pin is already present. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -GatedVersion + +Specify the version range for a Gating pin. When provided, a gating pin is created that limits +upgrades to versions satisfying this range. Uses WinGet version range syntax (e.g., `<7.5`, +`>=7.0,<8.0`). Cannot be combined with **-Blocking**. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Id + +Specify the package identifier to search for. By default, the command does a case-insensitive +exact match. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -MatchOption + +Specify the match option for a WinGet package query. This parameter accepts the following values: + +```yaml +Type: Microsoft.WinGet.Client.PSObjects.PSPackageFieldMatchOption +Parameter Sets: FoundSet +Aliases: +Accepted values: Equals, EqualsCaseInsensitive, StartsWithCaseInsensitive, ContainsCaseInsensitive + +Required: False +Position: Named +Default value: EqualsCaseInsensitive +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Moniker + +Specify the moniker of the WinGet package to search for. For example, the moniker for the +Microsoft.PowerShell package is `pwsh`. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Name + +Specify the name of the WinGet package to search for. If the name contains spaces, enclose the +name in quotes. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Note + +Specify an optional note to attach to the pin. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -PinInstalledPackage + +When specified, pins the currently installed version of the package rather than all versions. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Blocking + +When specified, adds a Blocking pin that prevents all upgrades, including manual ones. Cannot be +combined with **-GatedVersion**. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -PSCatalogPackage + +Provide a **PSCatalogPackage** object. You can get a **PSCatalogPackage** object by using the +`Find-WinGetPackage` or `Get-WinGetPackage` commands. + +```yaml +Type: Microsoft.WinGet.Client.Engine.PSObjects.PSCatalogPackage +Parameter Sets: GivenSet +Aliases: InputObject + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -Query + +Specify one or more strings to search for. By default, the command searches all configured sources. +Wildcards are not supported. The command compares the value provided to the following package +manifest properties: + + - `PackageIdentifier` + - `PackageName` + - `Moniker` + - `Tags` + +The command does a case-insensitive substring comparison of these properties. + +```yaml +Type: System.String[] +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Source + +Specify the name of a configured WinGet source. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -WhatIf + +Shows what would happen if the cmdlet runs. The cmdlet is not run. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### Microsoft.WinGet.Client.Engine.PSObjects.PSCatalogPackage + +### System.String + +### System.String[] + +### Microsoft.WinGet.Client.PSObjects.PSPackageFieldMatchOption + +### System.Management.Automation.SwitchParameter + +## OUTPUTS + +### Microsoft.WinGet.Client.Engine.PSObjects.PSPinResult + +## NOTES + +## RELATED LINKS + +[Get-WinGetPin](Get-WinGetPin.md) + +[Remove-WinGetPin](Remove-WinGetPin.md) + +[Reset-WinGetPin](Reset-WinGetPin.md) + +[Find-WinGetPackage](Find-WinGetPackage.md) + +[Get-WinGetPackage](Get-WinGetPackage.md) diff --git a/src/PowerShell/Help/Microsoft.WinGet.Client/Get-WinGetPin.md b/src/PowerShell/Help/Microsoft.WinGet.Client/Get-WinGetPin.md new file mode 100644 index 0000000000..09cb3000cf --- /dev/null +++ b/src/PowerShell/Help/Microsoft.WinGet.Client/Get-WinGetPin.md @@ -0,0 +1,221 @@ +--- +external help file: Microsoft.WinGet.Client.Cmdlets.dll-Help.xml +Module Name: Microsoft.WinGet.Client +ms.date: 08/01/2024 +online version: +schema: 2.0.0 +--- + +# Get-WinGetPin + +## SYNOPSIS +Gets WinGet package pins. + +## SYNTAX + +### AllSet (Default) +``` +Get-WinGetPin [] +``` + +### GivenSet +``` +Get-WinGetPin [[-PSCatalogPackage] ] [] +``` + +### FoundSet +``` +Get-WinGetPin [-Id ] [-Name ] [-Moniker ] [-Source ] + [[-Query] ] [-MatchOption ] [] +``` + +## DESCRIPTION +This command retrieves WinGet package pins. When called without parameters, it returns all pins. +When called with package search criteria or a catalog package, it returns pins for the matching +package. By default, all string-based searches are case-insensitive substring searches. +Wildcards are not supported. You can change the search behavior using the **MatchOption** parameter. + +## EXAMPLES + +### Example 1: Get all pins +```powershell +Get-WinGetPin +``` +This example gets all package pins configured in WinGet. + +### Example 2: Get the pin for a specific package by Id +```powershell +Get-WinGetPin -Id "Microsoft.PowerShell" +``` +This example gets the pin for the package with the identifier `Microsoft.PowerShell`. + +### Example 3: Get the pin for a package using a query +```powershell +Get-WinGetPin -Query "PowerShell" +``` +This example gets pins for packages matching the query `PowerShell`. + +### Example 4: Get the pin for an installed package using the pipeline +```powershell +Get-WinGetPackage -Id "Microsoft.PowerShell" | Get-WinGetPin +``` +This example gets the pin for an installed package passed from `Get-WinGetPackage`. + +## PARAMETERS + +### -Id + +Specify the package identifier to search for. By default, the command does a case-insensitive +substring match. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -MatchOption + +Specify the match option for a WinGet package query. This parameter accepts the following values: + +```yaml +Type: Microsoft.WinGet.Client.PSObjects.PSPackageFieldMatchOption +Parameter Sets: FoundSet +Aliases: +Accepted values: Equals, EqualsCaseInsensitive, StartsWithCaseInsensitive, ContainsCaseInsensitive + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Moniker + +Specify the moniker of the WinGet package to search for. For example, the moniker for the +Microsoft.PowerShell package is `pwsh`. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Name + +Specify the name of the WinGet package to search for. If the name contains spaces, enclose the +name in quotes. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -PSCatalogPackage + +Provide a **PSCatalogPackage** object. You can get a **PSCatalogPackage** object by using the +`Find-WinGetPackage` or `Get-WinGetPackage` commands. + +```yaml +Type: Microsoft.WinGet.Client.Engine.PSObjects.PSCatalogPackage +Parameter Sets: GivenSet +Aliases: InputObject + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -Query + +Specify one or more strings to search for. By default, the command searches all configured sources. +Wildcards are not supported. The command compares the value provided to the following package +manifest properties: + + - `PackageIdentifier` + - `PackageName` + - `Moniker` + - `Tags` + +The command does a case-insensitive substring comparison of these properties. + +```yaml +Type: System.String[] +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Source + +Specify the name of a configured WinGet source. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### Microsoft.WinGet.Client.Engine.PSObjects.PSCatalogPackage + +### System.String + +### System.String[] + +### Microsoft.WinGet.Client.PSObjects.PSPackageFieldMatchOption + +## OUTPUTS + +### Microsoft.WinGet.Client.Engine.PSObjects.PSPackagePin + +## NOTES + +## RELATED LINKS + +[Add-WinGetPin](Add-WinGetPin.md) + +[Remove-WinGetPin](Remove-WinGetPin.md) + +[Reset-WinGetPin](Reset-WinGetPin.md) + +[Find-WinGetPackage](Find-WinGetPackage.md) + +[Get-WinGetPackage](Get-WinGetPackage.md) diff --git a/src/PowerShell/Help/Microsoft.WinGet.Client/Microsoft.WinGet.Client.md b/src/PowerShell/Help/Microsoft.WinGet.Client/Microsoft.WinGet.Client.md index 9765cc16fb..c06de08783 100644 --- a/src/PowerShell/Help/Microsoft.WinGet.Client/Microsoft.WinGet.Client.md +++ b/src/PowerShell/Help/Microsoft.WinGet.Client/Microsoft.WinGet.Client.md @@ -13,6 +13,9 @@ title: Microsoft.WinGet.Client Module Microsoft WinGet Client Module for the Windows Package Manager ## Microsoft.WinGet.Client Cmdlets +### [Add-WinGetPin](Add-WinGetPin.md) +Adds a WinGet package pin. + ### [Add-WinGetSource](Add-WinGetSource.md) Adds a new WinGet source. @@ -34,6 +37,9 @@ Searches configured sources for packages. ### [Get-WinGetPackage](Get-WinGetPackage.md) Gets installed packages. +### [Get-WinGetPin](Get-WinGetPin.md) +Gets WinGet package pins. + ### [Get-WinGetSetting](Get-WinGetSetting.md) Gets WinGet settings. @@ -49,6 +55,9 @@ Gets the installed version of WinGet. ### [Install-WinGetPackage](Install-WinGetPackage.md) Install a WinGet Package. +### [Remove-WinGetPin](Remove-WinGetPin.md) +Removes a WinGet package pin. + ### [Remove-WinGetSource](Remove-WinGetSource.md) Removes a configured source. @@ -58,6 +67,9 @@ Repairs a WinGet Package. ### [Repair-WinGetPackageManager](Repair-WinGetPackageManager.md) Repairs the WinGet client. +### [Reset-WinGetPin](Reset-WinGetPin.md) +Resets all WinGet package pins. + ### [Reset-WinGetSource](Reset-WinGetSource.md) Resets default WinGet sources. diff --git a/src/PowerShell/Help/Microsoft.WinGet.Client/Remove-WinGetPin.md b/src/PowerShell/Help/Microsoft.WinGet.Client/Remove-WinGetPin.md new file mode 100644 index 0000000000..b55d58c831 --- /dev/null +++ b/src/PowerShell/Help/Microsoft.WinGet.Client/Remove-WinGetPin.md @@ -0,0 +1,276 @@ +--- +external help file: Microsoft.WinGet.Client.Cmdlets.dll-Help.xml +Module Name: Microsoft.WinGet.Client +ms.date: 08/01/2024 +online version: +schema: 2.0.0 +--- + +# Remove-WinGetPin + +## SYNOPSIS +Removes a WinGet package pin. + +## SYNTAX + +### FoundSet (Default) +``` +Remove-WinGetPin [-Id ] [-Name ] [-Moniker ] [-Source ] + [[-Query] ] [-MatchOption ] [-ProgressAction ] + [-WhatIf] [-Confirm] [] +``` + +### GivenSet +``` +Remove-WinGetPin [[-PSCatalogPackage] ] [-ProgressAction ] + [-WhatIf] [-Confirm] [] +``` + +### PinSet +``` +Remove-WinGetPin [[-PSPackagePin] ] [-ProgressAction ] + [-WhatIf] [-Confirm] [] +``` + +## DESCRIPTION +This command removes a pin for a WinGet package. Mirrors the behavior of `winget pin remove`. +The command accepts a **PSPackagePin** object from the pipeline (e.g., from `Get-WinGetPin`), +a **PSCatalogPackage** object from `Get-WinGetPackage` or `Find-WinGetPackage`, or package +search criteria. By default, all string-based searches are case-insensitive exact matches. +Wildcards are not supported. You can change the search behavior using the **MatchOption** parameter. + +## EXAMPLES + +### Example 1: Remove a pin by package Id +```powershell +Remove-WinGetPin -Id "Microsoft.PowerShell" +``` +This example removes the pin for the package with the identifier `Microsoft.PowerShell`. + +### Example 2: Remove a pin using the pipeline from Get-WinGetPin +```powershell +Get-WinGetPin -Id "Microsoft.PowerShell" | Remove-WinGetPin +``` +This example removes a pin passed from `Get-WinGetPin`. + +### Example 3: Remove all pins +```powershell +Get-WinGetPin | Remove-WinGetPin +``` +This example removes all pins by piping the output of `Get-WinGetPin` to `Remove-WinGetPin`. + +### Example 4: Remove a pin using the pipeline from Get-WinGetPackage +```powershell +Get-WinGetPackage -Id "Microsoft.PowerShell" | Remove-WinGetPin +``` +This example removes the pin for an installed package passed from `Get-WinGetPackage`. + +## PARAMETERS + +### -Confirm + +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Id + +Specify the package identifier to search for. By default, the command does a case-insensitive +exact match. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -MatchOption + +Specify the match option for a WinGet package query. This parameter accepts the following values: + +```yaml +Type: Microsoft.WinGet.Client.PSObjects.PSPackageFieldMatchOption +Parameter Sets: FoundSet +Aliases: +Accepted values: Equals, EqualsCaseInsensitive, StartsWithCaseInsensitive, ContainsCaseInsensitive + +Required: False +Position: Named +Default value: EqualsCaseInsensitive +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Moniker + +Specify the moniker of the WinGet package to search for. For example, the moniker for the +Microsoft.PowerShell package is `pwsh`. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Name + +Specify the name of the WinGet package to search for. If the name contains spaces, enclose the +name in quotes. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -PSCatalogPackage + +Provide a **PSCatalogPackage** object. You can get a **PSCatalogPackage** object by using the +`Find-WinGetPackage` or `Get-WinGetPackage` commands. + +```yaml +Type: Microsoft.WinGet.Client.Engine.PSObjects.PSCatalogPackage +Parameter Sets: GivenSet +Aliases: InputObject + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -PSPackagePin + +Provide a **PSPackagePin** object. You can get a **PSPackagePin** object by using the +`Get-WinGetPin` command. + +```yaml +Type: Microsoft.WinGet.Client.Engine.PSObjects.PSPackagePin +Parameter Sets: PinSet +Aliases: InputObject + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -Query + +Specify one or more strings to search for. By default, the command searches all configured sources. +Wildcards are not supported. The command compares the value provided to the following package +manifest properties: + + - `PackageIdentifier` + - `PackageName` + - `Moniker` + - `Tags` + +The command does a case-insensitive substring comparison of these properties. + +```yaml +Type: System.String[] +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Source + +Specify the name of a configured WinGet source. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -WhatIf + +Shows what would happen if the cmdlet runs. The cmdlet is not run. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### Microsoft.WinGet.Client.Engine.PSObjects.PSPackagePin + +### Microsoft.WinGet.Client.Engine.PSObjects.PSCatalogPackage + +### System.String + +### System.String[] + +### Microsoft.WinGet.Client.PSObjects.PSPackageFieldMatchOption + +## OUTPUTS + +### Microsoft.WinGet.Client.Engine.PSObjects.PSPinResult + +## NOTES + +## RELATED LINKS + +[Get-WinGetPin](Get-WinGetPin.md) + +[Add-WinGetPin](Add-WinGetPin.md) + +[Reset-WinGetPin](Reset-WinGetPin.md) + +[Find-WinGetPackage](Find-WinGetPackage.md) + +[Get-WinGetPackage](Get-WinGetPackage.md) diff --git a/src/PowerShell/Help/Microsoft.WinGet.Client/Reset-WinGetPin.md b/src/PowerShell/Help/Microsoft.WinGet.Client/Reset-WinGetPin.md new file mode 100644 index 0000000000..1142a29f5c --- /dev/null +++ b/src/PowerShell/Help/Microsoft.WinGet.Client/Reset-WinGetPin.md @@ -0,0 +1,132 @@ +--- +external help file: Microsoft.WinGet.Client.Cmdlets.dll-Help.xml +Module Name: Microsoft.WinGet.Client +ms.date: 08/01/2024 +online version: +schema: 2.0.0 +--- + +# Reset-WinGetPin + +## SYNOPSIS +Resets all WinGet package pins. + +## SYNTAX + +``` +Reset-WinGetPin [-Source ] [-Force] [-ProgressAction ] [-WhatIf] [-Confirm] + [] +``` + +## DESCRIPTION +This command resets all WinGet package pins, removing all pin records. Mirrors the behavior of +`winget pin reset`. You can scope the reset to a specific source by providing the **Source** +parameter. Use the **Force** parameter to skip the confirmation prompt. + +## EXAMPLES + +### Example 1: Reset all pins +```powershell +Reset-WinGetPin +``` +This example resets all package pins across all configured sources. + +### Example 2: Reset pins for a specific source +```powershell +Reset-WinGetPin -Source "winget" +``` +This example resets all package pins for the `winget` source. + +### Example 3: Reset all pins without confirmation +```powershell +Reset-WinGetPin -Force +``` +This example resets all pins without prompting for confirmation. + +## PARAMETERS + +### -Confirm + +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force + +When specified, skips the confirmation prompt and resets pins immediately. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Source + +Specify the name of a configured WinGet source to scope the reset. If not specified, pins +across all sources are reset. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -WhatIf + +Shows what would happen if the cmdlet runs. The cmdlet is not run. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### System.String + +## OUTPUTS + +### Microsoft.WinGet.Client.Engine.PSObjects.PSPinResult + +## NOTES + +## RELATED LINKS + +[Get-WinGetPin](Get-WinGetPin.md) + +[Add-WinGetPin](Add-WinGetPin.md) + +[Remove-WinGetPin](Remove-WinGetPin.md) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddPinCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddPinCmdlet.cs new file mode 100644 index 0000000000..15e489b2b4 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddPinCmdlet.cs @@ -0,0 +1,128 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Commands.Common; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; + using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Client.PSObjects; + + /// + /// Adds a pin for a package. Mirrors the behavior of winget pin add. + /// + /// + /// Pin type is determined by which parameters are provided, matching winget pin add behavior: + /// + /// present → Gating pin + /// present → Blocking pin + /// Neither → Pinning pin (default) + /// + /// + [Cmdlet( + VerbsCommon.Add, + Constants.WinGetNouns.Pin, + DefaultParameterSetName = Constants.FoundSet, + SupportsShouldProcess = true)] + [OutputType(typeof(PSPinResult))] + public sealed class AddPinCmdlet : PackageCmdlet + { + private PinPackageCommand command = null; + + /// + /// Gets or sets a value indicating whether to add a blocking pin, which prevents all upgrades. + /// Cannot be combined with . + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter Blocking { get; set; } + + /// + /// Gets or sets the gated version range, which creates a gating pin that limits upgrades to + /// versions satisfying this range (e.g., <7.5, >=7.0,<8.0). + /// Cannot be combined with . + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string GatedVersion { get; set; } + + /// + /// Gets or sets a value indicating whether to pin the installed version of the package. + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter PinInstalledPackage { get; set; } + + /// + /// Gets or sets a value indicating whether to force the pin even if an existing pin is present. + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter Force { get; set; } + + /// + /// Gets or sets an optional note to attach to the pin. + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public string Note { get; set; } + + /// + /// Adds a pin for the specified package. + /// + protected override void ProcessRecord() + { + if (this.Blocking && !string.IsNullOrEmpty(this.GatedVersion)) + { + this.ThrowTerminatingError(new ErrorRecord( + new System.ArgumentException("Cannot specify both -Blocking and -GatedVersion. Use -GatedVersion for a gating pin or -Blocking for a blocking pin."), + "ConflictingPinTypeParameters", + ErrorCategory.InvalidArgument, + null)); + } + + PSPackagePinType pinType = !string.IsNullOrEmpty(this.GatedVersion) ? PSPackagePinType.Gating + : this.Blocking ? PSPackagePinType.Blocking + : PSPackagePinType.Pinning; + + string target = this.PSCatalogPackage?.Id + ?? this.Id + ?? this.Name + ?? (this.Query != null ? string.Join(" ", this.Query) : null) + ?? "package"; + + if (!this.ShouldProcess(target)) + { + return; + } + + this.command = new PinPackageCommand( + this, + this.PSCatalogPackage, + this.Id, + this.Name, + this.Moniker, + this.Source, + this.Query); + this.command.Add( + this.MatchOption.ToString(), + pinType.ToString(), + this.GatedVersion, + this.PinInstalledPackage.ToBool(), + this.Force.ToBool(), + this.Note); + } + + /// + /// Interrupts currently running code within the command. + /// + protected override void StopProcessing() + { + if (this.command != null) + { + this.command.Cancel(); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetPinCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetPinCmdlet.cs new file mode 100644 index 0000000000..e53ecf8c37 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetPinCmdlet.cs @@ -0,0 +1,74 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Commands.Common; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; + using Microsoft.WinGet.Client.Engine.PSObjects; + + /// + /// Gets package pins. If no filter is provided, returns all pins. + /// + [Cmdlet( + VerbsCommon.Get, + Constants.WinGetNouns.Pin, + DefaultParameterSetName = Constants.AllSet)] + [OutputType(typeof(PSPackagePin))] + public sealed class GetPinCmdlet : FinderCmdlet + { + private PinPackageCommand command = null; + + /// + /// Gets or sets the package to retrieve pins for. + /// + [Alias("InputObject")] + [ValidateNotNull] + [Parameter( + ParameterSetName = Constants.GivenSet, + Position = 0, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSCatalogPackage PSCatalogPackage { get; set; } = null; + + /// + /// Retrieves pins based on the specified parameters. + /// + protected override void ProcessRecord() + { + this.command = new PinPackageCommand( + this, + this.PSCatalogPackage, + this.Id, + this.Name, + this.Moniker, + this.Source, + this.Query); + + if (this.ParameterSetName == Constants.AllSet) + { + this.command.GetAll(); + } + else + { + this.command.Get(this.MatchOption.ToString()); + } + } + + /// + /// Interrupts currently running code within the command. + /// + protected override void StopProcessing() + { + if (this.command != null) + { + this.command.Cancel(); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/PSObjects/PSPackagePinType.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/PSObjects/PSPackagePinType.cs new file mode 100644 index 0000000000..705bde2a49 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/PSObjects/PSPackagePinType.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.PSObjects +{ + /// + /// Must match the user-settable values of Microsoft.Management.Deployment.PackagePinType. + /// + public enum PSPackagePinType + { + /// + /// Pinning - prevents automatic updates to the current version. + /// + Pinning, + + /// + /// Blocking - prevents installation or upgrade of the package. + /// + Blocking, + + /// + /// Gating - limits updates to versions below the specified GatedVersion. + /// + Gating, + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RemovePinCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RemovePinCmdlet.cs new file mode 100644 index 0000000000..d00bf2070b --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RemovePinCmdlet.cs @@ -0,0 +1,88 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Commands.Common; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; + using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Client.PSObjects; + + /// + /// Removes a package pin. Mirrors the behavior of winget pin remove. + /// Accepts a from the pipeline (e.g., from Get-WinGetPin), + /// a from the pipeline, or package search criteria. + /// + [Cmdlet( + VerbsCommon.Remove, + Constants.WinGetNouns.Pin, + DefaultParameterSetName = Constants.FoundSet, + SupportsShouldProcess = true)] + [OutputType(typeof(PSPinResult))] + public sealed class RemovePinCmdlet : PackageCmdlet + { + private PinPackageCommand command = null; + + /// + /// Gets or sets the pin object to remove. Accepts pipeline input from Get-WinGetPin. + /// + [ValidateNotNull] + [Parameter( + ParameterSetName = Constants.PinSet, + Position = 0, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSPackagePin PSPackagePin { get; set; } = null; + + /// + /// Removes the pin for the specified package. + /// + protected override void ProcessRecord() + { + PSCatalogPackage catalogPackage = this.PSCatalogPackage; + string id = this.Id; + + if (this.ParameterSetName == Constants.PinSet) + { + id = this.PSPackagePin.PackageId; + } + + string target = id + ?? this.PSCatalogPackage?.Id + ?? this.Name + ?? (this.Query != null ? string.Join(" ", this.Query) : null) + ?? "package"; + + if (!this.ShouldProcess(target)) + { + return; + } + + this.command = new PinPackageCommand( + this, + catalogPackage, + id, + this.Name, + this.Moniker, + this.Source, + this.Query); + this.command.Remove(this.MatchOption.ToString()); + } + + /// + /// Interrupts currently running code within the command. + /// + protected override void StopProcessing() + { + if (this.command != null) + { + this.command.Cancel(); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ResetPinCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ResetPinCmdlet.cs new file mode 100644 index 0000000000..47543b6d9f --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ResetPinCmdlet.cs @@ -0,0 +1,73 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; + using Microsoft.WinGet.Client.Engine.PSObjects; + + /// + /// Resets all package pins, optionally scoped to a source. Mirrors the behavior of winget pin reset. + /// + [Cmdlet( + VerbsCommon.Reset, + Constants.WinGetNouns.Pin, + SupportsShouldProcess = true)] + [OutputType(typeof(PSPinResult))] + public sealed class ResetPinCmdlet : PSCmdlet + { + private ResetPinCommand command = null; + + /// + /// Gets or sets the source name to scope the reset. If not specified, all sources are reset. + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public string Source { get; set; } + + /// + /// Gets or sets a value indicating whether to skip confirmation. + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter Force { get; set; } + + /// + /// Resets pins. + /// + protected override void ProcessRecord() + { + string target = string.IsNullOrEmpty(this.Source) ? "All sources" : this.Source; + if (!this.ShouldProcess(target)) + { + return; + } + + if (!this.Force && !this.ShouldContinue( + string.IsNullOrEmpty(this.Source) + ? "This will reset all pins for all sources." + : $"This will reset all pins for source '{this.Source}'.", + "Confirm")) + { + return; + } + + this.command = new ResetPinCommand(this); + this.command.Reset(this.Source); + } + + /// + /// Interrupts currently running code within the command. + /// + protected override void StopProcessing() + { + if (this.command != null) + { + this.command.Cancel(); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Common/Constants.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Common/Constants.cs index 62847c6567..dca94fbc28 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Common/Constants.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Common/Constants.cs @@ -52,6 +52,16 @@ internal static class Constants /// public const string IntegrityLatestSet = "IntegrityLatestSet"; + /// + /// This parameter set indicates that no filter is applied and all items should be returned. + /// + public const string AllSet = "AllSet"; + + /// + /// This parameter set indicates that a pin object was provided via a parameter or the pipeline. + /// + public const string PinSet = "PinSet"; + /// /// Nouns used for different cmdlets. Changing this will alter the names of the related commands. /// @@ -86,6 +96,11 @@ public static class WinGetNouns /// The noun for enable/disable winget admin settings. /// public const string Setting = "WinGetSetting"; + + /// + /// WinGetPin. + /// + public const string Pin = "WinGetPin"; } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/FinderPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/FinderPackageCommand.cs index fdfa06ce3e..4d5f1e06d1 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/FinderPackageCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/FinderPackageCommand.cs @@ -6,6 +6,8 @@ namespace Microsoft.WinGet.Client.Engine.Commands { + using System.Collections.Generic; + using System.Linq; using System.Management.Automation; using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Commands.Common; @@ -82,9 +84,21 @@ public void Get(string psPackageFieldMatchOption) () => this.FindPackages( CompositeSearchBehavior.LocalCatalogs, PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption))); + + if (results.Count == 0) + { + return; + } + + // Fetch all pins in a single COM call and build a lookup set to avoid + // one GetPins() roundtrip per package when IsPinned is accessed during output. + IReadOnlyList allPins = this.Execute( + () => PackageManagerWrapper.Instance.GetAllPins()); + var pinnedPackageIds = allPins.Select(p => p.PackageId).ToHashSet(); + for (var i = 0; i < results.Count; i++) { - this.Write(StreamType.Object, new PSInstalledCatalogPackage(results[i].CatalogPackage)); + this.Write(StreamType.Object, new PSInstalledCatalogPackage(results[i].CatalogPackage, pinnedPackageIds)); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/PinPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/PinPackageCommand.cs new file mode 100644 index 0000000000..314a818acd --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/PinPackageCommand.cs @@ -0,0 +1,179 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Commands +{ + using System.Collections.Generic; + using System.Management.Automation; + using System.Threading.Tasks; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Engine.Commands.Common; + using Microsoft.WinGet.Client.Engine.Exceptions; + using Microsoft.WinGet.Client.Engine.Helpers; + using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Common.Command; + + /// + /// Engine command for pin operations (get, add, remove). + /// + public sealed class PinPackageCommand : PackageCommand + { + /// + /// Initializes a new instance of the class. + /// + /// Caller cmdlet. + /// PSCatalogPackage (optional). + /// Package identifier. + /// Name of package. + /// Moniker of package. + /// Source to search. If null, all are searched. + /// Match against any field of a package. + public PinPackageCommand( + PSCmdlet psCmdlet, + PSCatalogPackage psCatalogPackage, + string id, + string name, + string moniker, + string source, + string[] query) + : base(psCmdlet) + { + if (psCatalogPackage != null) + { + this.CatalogPackage = psCatalogPackage; + } + + this.Id = id; + this.Name = name; + this.Moniker = moniker; + this.Source = source; + this.Query = query; + } + + /// + /// Gets all pins. + /// + public void GetAll() + { + IReadOnlyList pins = this.Execute( + () => PackageManagerWrapper.Instance.GetAllPins()); + + for (int i = 0; i < pins.Count; i++) + { + this.Write(StreamType.Object, new PSPackagePin(pins[i])); + } + } + + /// + /// Gets pins for a specific package. + /// + /// PSPackageFieldMatchOption string. + public void Get(string psPackageFieldMatchOption) + { + IReadOnlyList pins = this.Execute(() => + { + CatalogPackage package = this.FindCatalogPackage( + CompositeSearchBehavior.AllCatalogs, + PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption)); + return PackageManagerWrapper.Instance.GetPins(package); + }); + + for (int i = 0; i < pins.Count; i++) + { + this.Write(StreamType.Object, new PSPackagePin(pins[i])); + } + } + + /// + /// Adds a pin for a package. + /// + /// PSPackageFieldMatchOption string. + /// Pin type string. + /// Gated version range (for Gating pins). + /// Whether to pin the installed package. + /// Whether to force the pin. + /// Optional user note. + public void Add( + string psPackageFieldMatchOption, + string pinType, + string gatedVersion, + bool pinInstalledPackage, + bool force, + string note) + { + var result = this.Execute( + async () => await this.GetPackageAndExecuteAsync( + CompositeSearchBehavior.AllCatalogs, + PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption), + async (package, version) => + { + var options = ManagementDeploymentFactory.Instance.CreatePinPackageOptions(); + options.PinType = PSEnumHelpers.ToPackagePinType(pinType); + if (!string.IsNullOrEmpty(gatedVersion)) + { + options.GatedVersion = gatedVersion; + } + + options.PinInstalledPackage = pinInstalledPackage; + options.Force = force; + if (!string.IsNullOrEmpty(note)) + { + options.Note = note; + } + + return await Task.FromResult(PackageManagerWrapper.Instance.PinPackage(package, options)); + })); + + if (result != null) + { + this.Write(StreamType.Object, new PSPinResult(result.Item1)); + } + } + + /// + /// Removes the pin for a package. + /// + /// PSPackageFieldMatchOption string. + public void Remove(string psPackageFieldMatchOption) + { + var result = this.Execute( + async () => await this.GetPackageAndExecuteAsync( + CompositeSearchBehavior.AllCatalogs, + PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption), + async (package, version) => + { + return await Task.FromResult(PackageManagerWrapper.Instance.UnpinPackage(package)); + })); + + if (result != null) + { + this.Write(StreamType.Object, new PSPinResult(result.Item1)); + } + } + + private CatalogPackage FindCatalogPackage(CompositeSearchBehavior behavior, PackageFieldMatchOption match) + { + if (this.CatalogPackage != null) + { + return this.CatalogPackage.CatalogPackageCOM; + } + + IReadOnlyList results = this.FindPackages(behavior, 0, match); + if (results.Count == 0) + { + throw new NoPackageFoundException(); + } + else if (results.Count == 1) + { + return results[0].CatalogPackage; + } + else + { + throw new VagueCriteriaException(results); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/ResetPinCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/ResetPinCommand.cs new file mode 100644 index 0000000000..d5f10e5144 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/ResetPinCommand.cs @@ -0,0 +1,50 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Commands +{ + using System.Management.Automation; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Engine.Commands.Common; + using Microsoft.WinGet.Client.Engine.Exceptions; + using Microsoft.WinGet.Client.Engine.Helpers; + using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Common.Command; + + /// + /// Engine command for resetting all pins. + /// + public sealed class ResetPinCommand : ManagementDeploymentCommand + { + /// + /// Initializes a new instance of the class. + /// + /// Caller cmdlet. + public ResetPinCommand(PSCmdlet psCmdlet) + : base(psCmdlet) + { + } + + /// + /// Resets all pins, optionally scoped to a source. + /// + /// The source name to scope the reset. Pass null or empty to reset all sources. + public void Reset(string sourceName) + { + PackageCatalogReference? catalogReference = null; + if (!string.IsNullOrEmpty(sourceName)) + { + catalogReference = PackageManagerWrapper.Instance.GetPackageCatalogByName(sourceName) + ?? throw new InvalidSourceException(sourceName); + } + + var result = this.Execute( + () => PackageManagerWrapper.Instance.ResetAllPins(catalogReference)); + + this.Write(StreamType.Object, new PSPinResult(result)); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ManagementDeploymentFactory.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ManagementDeploymentFactory.cs index 76d9121e4a..5528da6149 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ManagementDeploymentFactory.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ManagementDeploymentFactory.cs @@ -31,6 +31,7 @@ internal sealed class ManagementDeploymentFactory private static readonly Guid PackageMatchFilterClsid = Guid.Parse("D02C9DAF-99DC-429C-B503-4E504E4AB000"); private static readonly Guid DownloadOptionsClsid = Guid.Parse("4CBABE76-7322-4BE4-9CEA-2589A80682DC"); private static readonly Guid RepairOptionsClsid = Guid.Parse("0498F441-3097-455F-9CAF-148F28293865"); + private static readonly Guid PinPackageOptionsClsid = Guid.Parse("93409EF2-29D0-46D3-8085-13EDE73939C4"); #else private static readonly Guid PackageManagerClsid = Guid.Parse("74CB3139-B7C5-4B9E-9388-E6616DEA288C"); private static readonly Guid FindPackagesOptionsClsid = Guid.Parse("1BD8FF3A-EC50-4F69-AEEE-DF4C9D3BAA96"); @@ -40,6 +41,7 @@ internal sealed class ManagementDeploymentFactory private static readonly Guid PackageMatchFilterClsid = Guid.Parse("3F85B9F4-487A-4C48-9035-2903F8A6D9E8"); private static readonly Guid DownloadOptionsClsid = Guid.Parse("8EF324ED-367C-4880-83E5-BB2ABD0B72F6"); private static readonly Guid RepairOptionsClsid = Guid.Parse("E62BB1E7-C7B2-4AEC-9E28-FB649B30FF03"); + private static readonly Guid PinPackageOptionsClsid = Guid.Parse("B3A61CCB-A3D0-497D-B300-A904904EEA56"); #endif [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] private static readonly Type? PackageManagerType = Type.GetTypeFromCLSID(PackageManagerClsid); @@ -57,6 +59,8 @@ internal sealed class ManagementDeploymentFactory private static readonly Type? DownloadOptionsType = Type.GetTypeFromCLSID(DownloadOptionsClsid); [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] private static readonly Type? RepairOptionsType = Type.GetTypeFromCLSID(RepairOptionsClsid); + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] + private static readonly Type? PinPackageOptionsType = Type.GetTypeFromCLSID(PinPackageOptionsClsid); // These GUIDs correspond to the CSWinRT interface IIDs generated for Microsoft.Management.Deployment.Projection // and are auto-generated by the WinRT tool. @@ -68,6 +72,7 @@ internal sealed class ManagementDeploymentFactory private static readonly Guid PackageMatchFilterIid = Guid.Parse("D981ECA3-4DE5-5AD7-967A-698C7D60FC3B"); private static readonly Guid DownloadOptionsIid = Guid.Parse("94C92C4B-43F5-5CA3-BBBE-9F432C9546BC"); private static readonly Guid RepairOptionsIid = Guid.Parse("263F0546-2D7E-53A0-B8D1-75B74817FF18"); + private static readonly Guid PinPackageOptionsIid = Guid.Parse("8AB6949E-BB04-5F77-8B69-EF444D9C1635"); private static readonly IEnumerable ValidArchs = new Architecture[] { Architecture.X86, Architecture.X64 }; @@ -176,6 +181,15 @@ public RepairOptions CreateRepairOptions() return Create(RepairOptionsType, RepairOptionsIid); } + /// + /// Creates an instance of the class. + /// + /// A instance. + public PinPackageOptions CreatePinPackageOptions() + { + return Create(PinPackageOptionsType, PinPackageOptionsIid); + } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] private static T Create(Type? type, in Guid iid) where T : new() diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs index 2228a1c1c4..45e7e368f5 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs @@ -150,6 +150,22 @@ public static PackageRepairMode ToPackageRepairMode(string value) }; } + /// + /// Converts PSPackagePinType string value to PackagePinType. + /// + /// PSPackagePinType string value. + /// PackagePinType. + public static PackagePinType ToPackagePinType(string value) + { + return value switch + { + "Pinning" => PackagePinType.Pinning, + "Blocking" => PackagePinType.Blocking, + "Gating" => PackagePinType.Gating, + _ => throw new InvalidOperationException(), + }; + } + /// /// Converts PSWindowsPlatform string value to WindowsPlatform. /// diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs index f1ef5204e0..0d1c593c34 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs @@ -136,6 +136,66 @@ public PackageCatalogReference CreateCompositePackageCatalog(CreateCompositePack false); } + /// + /// Wrapper for GetPins. + /// + /// The package to get pins for. + /// A read-only list of PackagePin objects. + public IReadOnlyList GetPins(CatalogPackage package) + { + return this.Execute( + () => this.packageManager.GetPins(package), + false); + } + + /// + /// Wrapper for GetAllPins. + /// + /// A read-only list of all PackagePin objects. + public IReadOnlyList GetAllPins() + { + return this.Execute( + () => this.packageManager.GetAllPins(), + false); + } + + /// + /// Wrapper for PinPackage. + /// + /// The package to pin. + /// The pin options. + /// A PinPackageResult. + public PinPackageResult PinPackage(CatalogPackage package, PinPackageOptions options) + { + return this.Execute( + () => this.packageManager.PinPackage(package, options), + false); + } + + /// + /// Wrapper for UnpinPackage. + /// + /// The package to unpin. + /// A PinPackageResult. + public PinPackageResult UnpinPackage(CatalogPackage package) + { + return this.Execute( + () => this.packageManager.UnpinPackage(package), + false); + } + + /// + /// Wrapper for ResetAllPins. + /// + /// The catalog reference to reset pins for. Pass null to reset all. + /// A PinPackageResult. + public PinPackageResult ResetAllPins(PackageCatalogReference? packageCatalogReference) + { + return this.Execute( + () => this.packageManager.ResetAllPins(packageCatalogReference!), + false); + } + /// /// Gets the version of the package manager that is running. /// diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs index 3c3dcb7519..ef1d40a197 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs @@ -7,20 +7,30 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { using System; + using System.Collections.Generic; using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Engine.Helpers; /// /// InstalledCatalogPackage wrapper object for displaying to PowerShell. /// public sealed class PSInstalledCatalogPackage : PSCatalogPackage { + private readonly IReadOnlySet? pinnedPackageIds; + private bool? isPinned; + /// /// Initializes a new instance of the class. /// /// The catalog package COM object. - internal PSInstalledCatalogPackage(CatalogPackage catalogPackage) + /// + /// Optional pre-fetched set of pinned package IDs. When provided, + /// uses this set instead of issuing a per-package COM call. + /// + internal PSInstalledCatalogPackage(CatalogPackage catalogPackage, IReadOnlySet? pinnedPackageIds = null) : base(catalogPackage) { + this.pinnedPackageIds = pinnedPackageIds; } /// @@ -31,6 +41,24 @@ public string InstalledVersion get { return this.CatalogPackageCOM.InstalledVersion.Version; } } + /// + /// Gets a value indicating whether the package is pinned. + /// + public bool IsPinned + { + get + { + if (!this.isPinned.HasValue) + { + this.isPinned = this.pinnedPackageIds != null + ? this.pinnedPackageIds.Contains(this.CatalogPackageCOM.Id) + : PackageManagerWrapper.Instance.GetPins(this.CatalogPackageCOM).Count > 0; + } + + return this.isPinned.Value; + } + } + /// /// Compares versions. /// diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs new file mode 100644 index 0000000000..27a11477aa --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs @@ -0,0 +1,84 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.PSObjects +{ + using System; + using Microsoft.Management.Deployment; + + /// + /// PSPackagePin wraps a PackagePin COM object for PowerShell output. + /// + public sealed class PSPackagePin + { + private readonly PackagePin packagePin; + + /// + /// Initializes a new instance of the class. + /// + /// The PackagePin COM object. + internal PSPackagePin(PackagePin packagePin) + { + this.packagePin = packagePin; + } + + /// + /// Gets the package identifier. + /// + public string PackageId + { + get { return this.packagePin.PackageId; } + } + + /// + /// Gets the source identifier the pin applies to. + /// + public string SourceId + { + get { return this.packagePin.SourceId; } + } + + /// + /// Gets the pin type. + /// + public string Type + { + get { return this.packagePin.Type.ToString(); } + } + + /// + /// Gets the gated version range (for Gating pins only). + /// + public string GatedVersion + { + get { return this.packagePin.GatedVersion; } + } + + /// + /// Gets the UTC date/time when the pin was added. Null if not set. + /// + public DateTimeOffset? DateAdded + { + get { return this.packagePin.DateAdded; } + } + + /// + /// Gets the optional user note for this pin. + /// + public string Note + { + get { return this.packagePin.Note; } + } + + /// + /// Gets a value indicating whether this pin applies to an installed package. + /// + public bool IsForInstalledPackage + { + get { return this.packagePin.IsForInstalledPackage; } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPinResult.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPinResult.cs new file mode 100644 index 0000000000..fdfed20f94 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPinResult.cs @@ -0,0 +1,62 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.PSObjects +{ + using System; + using Microsoft.Management.Deployment; + + /// + /// PSPinResult wraps a PinPackageResult COM object for PowerShell output. + /// + public sealed class PSPinResult + { + private readonly PinPackageResult pinResult; + + /// + /// Initializes a new instance of the class. + /// + /// The PinPackageResult COM object. + internal PSPinResult(PinPackageResult pinResult) + { + this.pinResult = pinResult; + } + + /// + /// Gets the status of the pin operation. + /// + public string Status + { + get { return this.pinResult.Status.ToString(); } + } + + /// + /// Gets the extended error code of the pin operation. + /// + public Exception ExtendedErrorCode + { + get { return this.pinResult.ExtendedErrorCode; } + } + + /// + /// Returns whether the pin operation succeeded. + /// + /// True if the pin operation succeeded. + public bool Succeeded() + { + return this.pinResult.Status == PinResultStatus.Ok; + } + + /// + /// Returns a formatted error message. + /// + /// Error message string. + public string ErrorMessage() + { + return $"PinStatus: '{this.Status}' ExtendedError: '0x{this.ExtendedErrorCode.HResult:X8}'"; + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psd1 b/src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psd1 index 2603df2cfa..71e981fd0f 100644 --- a/src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psd1 +++ b/src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psd1 @@ -95,6 +95,10 @@ CmdletsToExport = @( 'Reset-WinGetSource' 'Export-WinGetPackage' 'Repair-WinGetPackage' + 'Get-WinGetPin' + 'Add-WinGetPin' + 'Remove-WinGetPin' + 'Reset-WinGetPin' ) # Variables to export from this module diff --git a/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 index f8042c2565..8fecf43d74 100644 --- a/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 +++ b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 @@ -1019,6 +1019,193 @@ Describe 'WindowsPackageManagerServer' -Skip:($PSEdition -eq "Desktop") { } } +Describe 'Add|Get|Remove|Reset-WinGetPin' { + + BeforeAll { + AddTestSource + } + + AfterAll { + Reset-WinGetPin -Force | Out-Null + RemoveTestSource + } + + Context 'Add-WinGetPin' { + + AfterEach { + Reset-WinGetPin -Force | Out-Null + } + + It 'Add pinning pin by Id' { + $result = Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Ok' + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -Not -BeNullOrEmpty + $pin.PackageId | Should -Be 'AppInstallerTest.TestExeInstaller' + $pin.Type | Should -Be 'Pinning' + } + + It 'Add blocking pin by Id' { + $result = Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' -Blocking + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Ok' + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -Not -BeNullOrEmpty + $pin.Type | Should -Be 'Blocking' + } + + It 'Add gating pin with GatedVersion' { + $result = Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' -GatedVersion '<2.0.0.0' + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Ok' + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -Not -BeNullOrEmpty + $pin.Type | Should -Be 'Gating' + $pin.GatedVersion | Should -Be '<2.0.0.0' + } + + It 'Add pin with Note' { + $result = Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' -Note 'test note' + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Ok' + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -Not -BeNullOrEmpty + $pin.Note | Should -Be 'test note' + } + + It 'Add with -Blocking and -GatedVersion throws' { + { Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' -Blocking -GatedVersion '<2.0.0.0' } | Should -Throw + } + + It 'Add with -WhatIf does not create pin' { + Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' -WhatIf + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -BeNullOrEmpty + } + } + + Context 'Get-WinGetPin' { + + BeforeAll { + Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' | Out-Null + } + + AfterAll { + Reset-WinGetPin -Force | Out-Null + } + + It 'Get all pins returns non-empty list' { + $pins = Get-WinGetPin + $pins | Should -Not -BeNullOrEmpty + } + + It 'Get pin by Id' { + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + + $pin | Should -Not -BeNullOrEmpty + $pin.PackageId | Should -Be 'AppInstallerTest.TestExeInstaller' + } + + It 'Get pin scoped to Source' { + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + + $pin | Should -Not -BeNullOrEmpty + $pin.PackageId | Should -Be 'AppInstallerTest.TestExeInstaller' + $pin.SourceId | Should -Not -BeNullOrEmpty + } + + It 'Get pin via pipeline from Find-WinGetPackage' { + $pin = Find-WinGetPackage -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' -MatchOption Equals | Get-WinGetPin + + $pin | Should -Not -BeNullOrEmpty + $pin.PackageId | Should -Be 'AppInstallerTest.TestExeInstaller' + } + } + + Context 'Remove-WinGetPin' { + + BeforeEach { + Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' | Out-Null + } + + AfterEach { + Reset-WinGetPin -Force | Out-Null + } + + It 'Remove pin by Id' { + $result = Remove-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Ok' + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -BeNullOrEmpty + } + + It 'Remove pin via pipeline from Get-WinGetPin' { + Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' | Remove-WinGetPin + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -BeNullOrEmpty + } + + It 'Remove with -WhatIf does not remove pin' { + Remove-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' -WhatIf + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -Not -BeNullOrEmpty + } + } + + Context 'Reset-WinGetPin' { + + BeforeEach { + Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' | Out-Null + } + + It 'Reset all pins with -Force' { + $result = Reset-WinGetPin -Force + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Ok' + + $pins = Get-WinGetPin + $pins | Should -BeNullOrEmpty + } + + It 'Reset with -Source scopes to that source' { + $result = Reset-WinGetPin -Source 'TestSource' -Force + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Ok' + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -BeNullOrEmpty + } + + It 'Reset with -WhatIf does not remove pins' { + Reset-WinGetPin -WhatIf + + $pins = Get-WinGetPin + $pins | Should -Not -BeNullOrEmpty + } + + AfterEach { + Reset-WinGetPin -Force | Out-Null + } + } +} + AfterAll { RestoreWinGetSettings RemoveTestSource