diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index d4667a1c54..b6a47f3d0e 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -30,6 +30,7 @@ APPEXECLINK appinstallertest applic appname +appone appshutdown APPTERMINATION archs @@ -345,6 +346,7 @@ megamorf microsoftentraid microsoftentraidforazureblobstorage midl +migratepintable minidump MINORVERSION missingdependency @@ -438,6 +440,7 @@ Pherson pid pidl pidlist +pintable PKCS pkgmgr pkindex diff --git a/.github/instructions/sqlite-builder.instructions.md b/.github/instructions/sqlite-builder.instructions.md new file mode 100644 index 0000000000..0aa634c165 --- /dev/null +++ b/.github/instructions/sqlite-builder.instructions.md @@ -0,0 +1,114 @@ +--- +applyTo: "src/AppInstallerRepositoryCore/Microsoft/Schema/*, src/Microsoft.Management.Configuration/Database/Schema/*" +--- + +## 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::[::]` + - `AppInstaller::CLI::Execution` - CLI execution context + - `AppInstaller::CLI::Workflow` - Workflow functions + - `AppInstaller::Repository` - Repository/source logic + - `AppInstaller::Manifest` - Manifest types + - `AppInstaller::Settings` - User/admin settings + +- **Macros**: Prefixed with `AICLI_` for CLI, `WINGET_` for general +- **Data keys**: ExecutionContextData uses enum keys to type-safely store/retrieve data diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index a468b60c74..9921f5e395 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -1,5 +1,14 @@ ## New in v1.30 +## New Features + +### Pinning improvements + +- `winget pin add` now supports an optional `--note` value for pin metadata. +- `winget pin add` now tracks the date that a pin was added or updated. +- `winget pin list` now supports an optional `--details` argument for showing extended pin data. +- The PowerShell pin cmdlets now expose the new pinning capabilities and metadata. + ### `--ignore-unavailable` flag for `install` Added a new `--ignore-unavailable` flag to the `install` command. When installing multiple packages, this flag allows the operation to continue with the remaining packages instead of failing entirely when one or more packages are not found in the configured sources. This brings the same behavior previously available with `import --ignore-unavailable` to direct multi-package installs. diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index c9de94dad0..b5ed963413 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 33df634aa7..8d17e55e9e 100644 --- a/src/AppInstallerCLICore/Commands/PinCommand.cpp +++ b/src/AppInstallerCLICore/Commands/PinCommand.cpp @@ -65,6 +65,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 +344,5 @@ namespace AppInstaller::CLI Workflow::ReportPins; } } + } diff --git a/src/AppInstallerCLICore/Commands/PinCommand.h b/src/AppInstallerCLICore/Commands/PinCommand.h index fefedcbe77..d591e8f149 100644 --- a/src/AppInstallerCLICore/Commands/PinCommand.h +++ b/src/AppInstallerCLICore/Commands/PinCommand.h @@ -87,4 +87,5 @@ namespace AppInstaller::CLI protected: void ExecuteInternal(Execution::Context& context) const override; }; + } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index e1e6355cb9..3b15e86db0 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 68e194d5f1..5c58d6b16a 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -541,6 +541,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); @@ -548,6 +549,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); @@ -557,6 +560,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PinResetSuccessful); WINGET_DEFINE_RESOURCE_STRINGID(PinResettingAll); WINGET_DEFINE_RESOURCE_STRINGID(PinResetUseForceArg); + 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 b2522b6e0a..7e21ae71d9 100644 --- a/src/AppInstallerCLICore/Workflows/PinFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PinFlow.cpp @@ -4,6 +4,7 @@ #include "Resources.h" #include "PinFlow.h" #include "TableOutput.h" +#include #include #include #include @@ -14,6 +15,18 @@ namespace AppInstaller::CLI::Workflow { namespace { + struct PinRowData + { + Utility::LocIndString PackageName; + Utility::LocIndString PackageId; + Utility::LocIndString Version; + Utility::LocIndString SourceName; + Utility::LocIndString PinType; + Utility::LocIndString PinnedVersion; + Utility::LocIndString DateAdded; + Utility::LocIndString Note; + }; + // Creates a Pin appropriate for the context based on the arguments provided Pinning::Pin CreatePin(Execution::Context& context, const Pinning::PinKey& pinKey) { @@ -95,6 +108,53 @@ namespace AppInstaller::CLI::Workflow return searchRequest; } + + bool IsMatchForListQuery(const Pinning::Pin& pin, Execution::Context& context) + { + const auto& packageId = pin.GetKey().PackageId; + + bool hasId = context.Args.Contains(Execution::Args::Type::Id); + bool exactMatch = context.Args.Contains(Execution::Args::Type::Exact); + + if (hasId) + { + std::string_view idArg = context.Args.GetArg(Execution::Args::Type::Id); + return exactMatch + ? Utility::CaseInsensitiveEquals(packageId, idArg) + : Utility::CaseInsensitiveContainsSubstring(packageId, idArg); + } + + return true; + } + + bool IsMatchForListQuery(const PinRowData& row, Execution::Context& context) + { + 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); + + if (!hasName && !hasQuery) + { + return true; + } + + std::string_view queryArg = hasName + ? context.Args.GetArg(Execution::Args::Type::Name) + : context.Args.GetArg(Execution::Args::Type::Query); + + const auto packageName = static_cast(row.PackageName); + if (hasName) + { + return exactMatch + ? Utility::CaseInsensitiveEquals(packageName, queryArg) + : Utility::CaseInsensitiveContainsSubstring(packageName, queryArg); + } + + const auto packageId = static_cast(row.PackageId); + return exactMatch + ? (Utility::CaseInsensitiveEquals(packageName, queryArg) || Utility::CaseInsensitiveEquals(packageId, queryArg)) + : (Utility::CaseInsensitiveContainsSubstring(packageName, queryArg) || Utility::CaseInsensitiveContainsSubstring(packageId, queryArg)); + } } void OpenPinningIndex::operator()(Execution::Context& context) const @@ -197,9 +257,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); } @@ -238,24 +307,39 @@ namespace AppInstaller::CLI::Workflow void ReportPins(Execution::Context& context) { const auto& pins = context.Get(); - if (pins.empty()) + bool hasListQuery = context.Args.Contains(Execution::Args::Type::Query) || + context.Args.Contains(Execution::Args::Type::Id) || + context.Args.Contains(Execution::Args::Type::Name); + std::vector filteredPins; + filteredPins.reserve(pins.size()); + + for (const auto& pin : pins) { - context.Reporter.Info() << Resource::String::PinNoPinsExist << std::endl; - return; + if (IsMatchForListQuery(pin, context)) + { + filteredPins.push_back(pin); + } } - Execution::TableOutput<6> table(context.Reporter, + if (filteredPins.empty()) + { + if (hasListQuery) { - Resource::String::SearchName, - Resource::String::SearchId, - Resource::String::SearchVersion, - Resource::String::SearchSource, - Resource::String::PinType, - Resource::String::PinVersion, - }); + context.Reporter.Info() << Resource::String::PinShowNoMatchFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST); + } + else + { + context.Reporter.Info() << Resource::String::PinNoPinsExist << std::endl; + } + + return; + } + std::vector rowData; + rowData.reserve(filteredPins.size()); const auto& source = context.Get(); - for (const auto& pin : pins) + for (const auto& pin : filteredPins) { const auto& pinKey = pin.GetKey(); auto searchRequest = GetSearchRequestForPin(pin.GetKey()); @@ -292,15 +376,70 @@ namespace AppInstaller::CLI::Workflow version = installedVersion->GetProperty(PackageVersionProperty::Version); } - table.OutputLine({ - packageName, - pinKey.PackageId, - version, - sourceName, - std::string{ ToString(pin.GetType()) }, - pin.GetGatedVersion().ToString(), - }); + Utility::LocIndString dateAdded; + const auto& pinDateAdded = pin.GetDateAdded(); + if (pinDateAdded.has_value()) + { + dateAdded = Utility::LocIndString{ Utility::TimePointToString(*pinDateAdded, + Utility::TimeFacet::Year | Utility::TimeFacet::Month | Utility::TimeFacet::Day | + Utility::TimeFacet::Hour | Utility::TimeFacet::Minute | Utility::TimeFacet::Second) }; + } + + Utility::LocIndString note; + const auto& pinNote = pin.GetNote(); + if (pinNote.has_value() && !pinNote->empty()) + { + note = Utility::LocIndString{ *pinNote }; + } + + PinRowData rowDataEntry{ + std::move(packageName), + Utility::LocIndString{ pinKey.PackageId }, + std::move(version), + std::move(sourceName), + Utility::LocIndString{ std::string{ ToString(pin.GetType()) } }, + Utility::LocIndString{ pin.GetGatedVersion().ToString() }, + std::move(dateAdded), + std::move(note), + }; + + if (IsMatchForListQuery(rowDataEntry, context)) + { + rowData.push_back(std::move(rowDataEntry)); + } + } + } + + if (rowData.empty()) + { + if (hasListQuery) + { + context.Reporter.Info() << Resource::String::PinShowNoMatchFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST); + } + else + { + context.Reporter.Info() << Resource::String::PinNoPinsExist << std::endl; } + + return; + } + + Execution::TableOutput<8> table(context.Reporter, + { + Resource::String::SearchName, + Resource::String::SearchId, + Resource::String::SearchVersion, + Resource::String::SearchSource, + Resource::String::PinType, + Resource::String::PinVersion, + Resource::String::PinDateAdded, + Resource::String::PinNote, + }); + + for (const auto& row : rowData) + { + table.OutputLine({ row.PackageName, row.PackageId, row.Version, row.SourceName, row.PinType, row.PinnedVersion, row.DateAdded, row.Note }); } table.Complete(); diff --git a/src/AppInstallerCLIPackage/Package.appxmanifest b/src/AppInstallerCLIPackage/Package.appxmanifest index aa37c3b184..78de4d13b7 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 31d153dffa..2877074dd1 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,18 @@ 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 + Table header shown with `winget pin list --details` for when the pin was added or last updated. + + + Note + Table header shown with `winget pin list --details` for the user-provided note stored with the pin. + + + No pin found matching the specified criteria. + Shown when a filtered `winget pin list` command 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 ecab34392d..b6f68a201a 100644 --- a/src/AppInstallerCLITests/PinFlow.cpp +++ b/src/AppInstallerCLITests/PinFlow.cpp @@ -3,6 +3,10 @@ #include "pch.h" #include "WorkflowCommon.h" #include "TestHooks.h" +#include "PinTestCommon.h" +#include "TestSource.h" +#include +#include #include #include #include @@ -14,7 +18,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 +45,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 +203,276 @@ 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 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 +{ + AppInstaller::Manifest::Manifest MakeListTestManifest(std::string_view id) + { + AppInstaller::Manifest::Manifest result; + result.Id = std::string{ id }; + result.DefaultLocalization.Add(std::string{ id }); + result.DefaultLocalization.Add("TestPublisher"); + result.Version = "1.0.0"; + result.Installers.push_back({}); + return result; + } + + TestSourceResult MakeListTestSourceResult(std::string_view id) + { + std::string packageId{ id }; + return TestSourceResult( + packageId, + [packageId](std::vector& matches, std::weak_ptr source) + { + auto manifest = MakeListTestManifest(packageId); + matches.emplace_back( + AppInstaller::Repository::ResultMatch( + TestCompositePackage::Make(std::vector{ manifest }, source), + AppInstaller::Repository::PackageMatchFilter( + AppInstaller::Repository::PackageMatchField::Id, + AppInstaller::Repository::MatchType::Exact, + packageId))); + }); + } + + std::shared_ptr CreateListTestSource(std::initializer_list ids) + { + std::vector results; + results.reserve(ids.size()); + for (auto id : ids) + { + results.emplace_back(MakeListTestSourceResult(id)); + } + + return CreateTestSource(std::move(results)); + } + + void PopulatePinIndexForList(const std::filesystem::path& indexPath, const std::vector& pins, AppInstaller::SQLite::Version version = AppInstaller::SQLite::Version::Latest()) + { + PinningIndex index = PinningIndex::CreateNew(indexPath.u8string(), version); + for (const auto& pin : pins) + { + index.AddPin(pin); + } + } +} + +TEST_CASE("PinFlow_List_Filter_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)); + PopulatePinIndexForList(indexFile.GetPath(), { existingPin }); + + std::ostringstream listOutput; + TestContext listContext{ listOutput, std::cin }; + listContext.Args.AddArg(Execution::Args::Type::Query, "ThisQueryMatchesNothing"sv); + OverrideForCompositeInstalledSource(listContext, CreateListTestSource({ "SomePackage.Id" })); + + PinListCommand pinList({}); + pinList.Execute(listContext); + INFO(listOutput.str()); + + REQUIRE_TERMINATED_WITH(listContext, APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST); + REQUIRE(listOutput.str().find(Resource::LocString(Resource::String::PinShowNoMatchFound)) != std::string::npos); +} + +TEST_CASE("PinFlow_List_Filter_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" }); + PopulatePinIndexForList(indexFile.GetPath(), { pin }); + + std::ostringstream listOutput; + TestContext listContext{ listOutput, std::cin }; + listContext.Args.AddArg(Execution::Args::Type::Id, "MyApp.Package"sv); + OverrideForCompositeInstalledSource(listContext, CreateListTestSource({ "MyApp.Package" })); + + PinListCommand pinList({}); + pinList.Execute(listContext); + INFO(listOutput.str()); + + REQUIRE_FALSE(listContext.IsTerminated()); + REQUIRE(listOutput.str().find("MyApp.Package") != std::string::npos); + REQUIRE(listOutput.str().find("Blocking") != std::string::npos); + REQUIRE(listOutput.str().find(Resource::LocString(Resource::String::PinDateAdded)) != std::string::npos); + REQUIRE(listOutput.str().find("keep this one") != std::string::npos); +} + +TEST_CASE("PinFlow_List_Filter_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)); + PopulatePinIndexForList(indexFile.GetPath(), { pin }); + + std::ostringstream listOutput; + TestContext listContext{ listOutput, std::cin }; + // Partial, case-insensitive match on the package ID + listContext.Args.AddArg(Execution::Args::Type::Query, "appone"sv); + OverrideForCompositeInstalledSource(listContext, CreateListTestSource({ "Contoso.AppOne" })); + + PinListCommand pinList({}); + pinList.Execute(listContext); + INFO(listOutput.str()); + + REQUIRE_FALSE(listContext.IsTerminated()); + REQUIRE(listOutput.str().find("Contoso.AppOne") != std::string::npos); +} + +TEST_CASE("PinFlow_List_Filter_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)); + + PopulatePinIndexForList(indexFile.GetPath(), { pinA, pinB }); + + std::ostringstream listOutput; + TestContext listContext{ listOutput, std::cin }; + listContext.Args.AddArg(Execution::Args::Type::Id, "Vendor.App"sv); + listContext.Args.AddArg(Execution::Args::Type::Exact); + OverrideForCompositeInstalledSource(listContext, CreateListTestSource({ "Vendor.App", "Vendor.AppExtra" })); + + PinListCommand pinList({}); + pinList.Execute(listContext); + INFO(listOutput.str()); + + REQUIRE_FALSE(listContext.IsTerminated()); + // Only the exact-match pin should appear + REQUIRE(listOutput.str().find("Vendor.App") != std::string::npos); + // The inexact match should NOT appear + REQUIRE(listOutput.str().find("Vendor.AppExtra") == std::string::npos); +} + +TEST_CASE("PinFlow_List_DetailsWithoutNotes_HidesEmptyNoteColumn", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + Pin pin = Pin::CreatePinningPin({ "Example.Package", "src" }); + pin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::May2026_01_0800)); + // note intentionally not set + PopulatePinIndexForList(indexFile.GetPath(), { pin }); + + std::ostringstream listOutput; + TestContext listContext{ listOutput, std::cin }; + listContext.Args.AddArg(Execution::Args::Type::Query, "Example.Package"sv); + OverrideForCompositeInstalledSource(listContext, CreateListTestSource({ "Example.Package" })); + + PinListCommand pinList({}); + pinList.Execute(listContext); + INFO(listOutput.str()); + + REQUIRE_FALSE(listContext.IsTerminated()); + REQUIRE(listOutput.str().find("Example.Package") != std::string::npos); + REQUIRE(listOutput.str().find(Resource::LocString(Resource::String::PinDateAdded)) != std::string::npos); + REQUIRE(listOutput.str().find(Resource::LocString(Resource::String::PinNote)) == std::string::npos); +} + +TEST_CASE("PinFlow_List_V1_0Index_NoMigrationRequired", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + Pin pin = Pin::CreateBlockingPin({ "Legacy.Package", "src" }); + PopulatePinIndexForList(indexFile.GetPath(), { pin }, { 1, 0 }); + + std::ostringstream listOutput; + TestContext listContext{ listOutput, std::cin }; + listContext.Args.AddArg(Execution::Args::Type::Query, "Legacy.Package"sv); + OverrideForCompositeInstalledSource(listContext, CreateListTestSource({ "Legacy.Package" })); + + PinListCommand pinList({}); + pinList.Execute(listContext); + INFO(listOutput.str()); + + REQUIRE_FALSE(listContext.IsTerminated()); + REQUIRE(listOutput.str().find("Legacy.Package") != std::string::npos); + REQUIRE(listOutput.str().find(Resource::LocString(Resource::String::PinDateAdded)) == std::string::npos); + REQUIRE(listOutput.str().find(Resource::LocString(Resource::String::PinNote)) == std::string::npos); +} + +TEST_CASE("PinFlow_List_Filter_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 listOutput; + TestContext listContext{ listOutput, std::cin }; + listContext.Args.AddArg(Execution::Args::Type::Query, "AnyQuery"sv); + OverrideForCompositeInstalledSource(listContext, CreateListTestSource({})); + + PinListCommand pinList({}); + pinList.Execute(listContext); + INFO(listOutput.str()); + + REQUIRE_TERMINATED_WITH(listContext, APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST); +} + +TEST_CASE("PinFlow_List_DefaultOutput_ShowsAllColumns", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + Pin pin = Pin::CreateBlockingPin({ "Has.Note.Package", "sourceId" }); + pin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jun2026_01_0900)); + pin.SetNote(std::string{ "some note" }); + PopulatePinIndexForList(indexFile.GetPath(), { pin }); + + std::ostringstream listOutput; + TestContext listContext{ listOutput, std::cin }; + listContext.Args.AddArg(Execution::Args::Type::Query, "Has.Note.Package"sv); + OverrideForCompositeInstalledSource(listContext, CreateListTestSource({ "Has.Note.Package" })); + + PinListCommand pinList({}); + pinList.Execute(listContext); + INFO(listOutput.str()); + + REQUIRE_FALSE(listContext.IsTerminated()); + REQUIRE(listOutput.str().find("Has.Note.Package") != std::string::npos); + REQUIRE(listOutput.str().find(Resource::LocString(Resource::String::PinDateAdded)) != std::string::npos); + REQUIRE(listOutput.str().find(Resource::LocString(Resource::String::PinNote)) != std::string::npos); +} 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 a2930cb453..d6dd3bdab6 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,166 @@ 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_AddDuplicatePin", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Pin pin = Pin::CreateGatingPin({ "pkg", "src" }, { "1.0.*"sv }); + pin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1030)); + pin.SetNote(std::string{ "duplicate test" }); + + PinningIndex index = PinningIndex::CreateNew(tempFile, Version::Latest()); + index.AddPin(pin); + + REQUIRE_THROWS(index.AddPin(pin), ERROR_ALREADY_EXISTS); +} + + +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 10fd960372..cecdc51b6a 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); } @@ -535,6 +535,39 @@ TEST_CASE("SQLBuilder_Update", "[sqlbuilder]") SelectFromSimpleTestTableOnlyOneRow(connection, firstVal, secondVal); } +TEST_CASE("SQLBuilder_UpdateEmptyOptional", "[sqlbuilder]") +{ + Connection connection = Connection::Create(SQLITE_MEMORY_DB_CONNECTION_TARGET, Connection::OpenDisposition::Create); + + CreateSimpleTestTable(connection); + + int firstVal = 1; + std::string secondVal = "test"; + + InsertIntoSimpleTestTable(connection, firstVal, secondVal); + + std::optional emptySecondVal; + + Builder::StatementBuilder update; + update.Update(s_tableName) + .Set() + .Column(s_secondColumn).AssignValue(emptySecondVal) + .Where(s_firstColumn).Equals(firstVal); + + update.Execute(connection); + + Builder::StatementBuilder select; + select.Select({ s_firstColumn, s_secondColumn }).From(s_tableName).Where(s_firstColumn).Equals(firstVal); + + auto statement = select.Prepare(connection); + + REQUIRE(statement.Step()); + REQUIRE(statement.GetColumn(0) == firstVal); + REQUIRE(statement.GetColumnIsNull(1)); + + REQUIRE(!statement.Step()); +} + TEST_CASE("SQLBuilder_CaseInsensitive", "[sqlbuilder]") { Connection connection = Connection::Create(SQLITE_MEMORY_DB_CONNECTION_TARGET, Connection::OpenDisposition::Create); diff --git a/src/AppInstallerCommonCore/Public/winget/Pin.h b/src/AppInstallerCommonCore/Public/winget/Pin.h index 74df4947ac..1df9c5b748 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 3a5a39da6a..635201f8ab 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -338,6 +338,8 @@ + + @@ -451,6 +453,8 @@ + + diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters index 64f1b186c2..ae35be7dda 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} @@ -381,6 +384,12 @@ Microsoft\Schema\Pinning_1_0 + + Microsoft\Schema\Pinning_1_1 + + + Microsoft\Schema\Pinning_1_1 + Public\winget @@ -707,6 +716,9 @@ Microsoft\Schema\Pinning_1_0 + + Microsoft\Schema\Pinning_1_1 + Source Files @@ -740,6 +752,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 9494fb6643..ab56699631 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 { @@ -166,6 +167,24 @@ namespace AppInstaller::Repository::Microsoft savepoint.Commit(); } + bool PinningIndex::TryRemovePin(const Pinning::PinKey& pinKey) + { + std::lock_guard lockInterface{ *m_interfaceLock }; + AICLI_LOG(Repo, Verbose, << "Trying to remove Pin " << pinKey.ToString()); + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "pinningIndex_tryRemovePin"); + + bool result = m_interface->TryRemovePin(m_dbconn, pinKey); + + if (result) + { + SetLastWriteTime(); + savepoint.Commit(); + } + + return result; + } + std::optional PinningIndex::GetPin(const Pinning::PinKey& pinKey) { std::lock_guard lockInterface{ *m_interfaceLock }; @@ -184,15 +203,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 +224,42 @@ 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(); + m_interface = std::move(latestInterface); + AICLI_LOG(Repo, Info, << "Pinning Index migration successful"); + } + else + { + AICLI_LOG(Repo, Warning, << "Pinning Index migration failed; continuing with existing schema version [" << m_version << "]"); + } + } + } } 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 96d2019f52..7e37eaa4fb 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h @@ -51,6 +51,9 @@ namespace AppInstaller::Repository::Microsoft // Removes a pin from the index. void RemovePin(const Pinning::PinKey& pinKey); + // Attempts to remove a pin from the index and returns whether it existed. + bool TryRemovePin(const Pinning::PinKey& pinKey); + // Returns the current pin for a given package if it exists. std::optional GetPin(const Pinning::PinKey& pinKey); @@ -67,8 +70,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 cccbcd2c6d..6d1f104d9d 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 a86a443bdd..4ad42c0bad 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 d7c93bf177..88a9a2c80c 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 76f650db82..cf22ae15ff 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 cf253fd8a1..294baf3905 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 ae0d43bbe3..52d92ba8a5 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 2daced6a3e..7b1b3c539a 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; @@ -28,6 +32,10 @@ namespace AppInstaller::Repository::Microsoft::Schema // Removes a pin from the index. virtual SQLite::rowid_t RemovePin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) = 0; + // Attempts to remove a pin from the index. + // Returns false if no matching pin exists. + virtual bool TryRemovePin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) = 0; + // Returns the current pin for a given package if it exists. virtual std::optional GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) = 0; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp index f540645553..27fd5f4df8 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp @@ -30,14 +30,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 } } - 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_Index = "pin_index"sv; - std::string_view PinTable::TableName() { return s_PinTable_Table_Name; @@ -113,10 +105,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 59476d478f..d634e5e24a 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.h @@ -10,6 +10,13 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 { struct PinTable { + static inline constexpr std::string_view s_PinTable_Table_Name = "pin"; + static inline constexpr std::string_view s_PinTable_PackageId_Column = "package_id"; + static inline constexpr std::string_view s_PinTable_SourceId_Column = "source_id"; + static inline constexpr std::string_view s_PinTable_Type_Column = "type"; + static inline constexpr std::string_view s_PinTable_Version_Column = "version"; + static inline constexpr std::string_view s_PinTable_Index = "pin_index"; + // Get the table name. static std::string_view TableName(); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h index 50dc138d1c..0d73f0236a 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h @@ -10,11 +10,13 @@ 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; + std::pair UpdatePin(SQLite::Connection& connection, const Pinning::Pin& pin) override; SQLite::rowid_t RemovePin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) override; + bool TryRemovePin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) override; 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; 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 1cf25a345d..bae931a7e3 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()); @@ -78,6 +84,18 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 return existingPinId.value(); } + bool PinningIndexInterface::TryRemovePin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) + { + auto existingPinId = GetExistingPinId(connection, pinKey); + if (!existingPinId) + { + return false; + } + + PinTable::RemovePinById(connection, existingPinId.value()); + return true; + } + std::optional PinningIndexInterface::GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) { auto existingPinId = GetExistingPinId(connection, pinKey); @@ -103,4 +121,4 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 return result; } -} \ No newline at end of file +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.h new file mode 100644 index 0000000000..6521bb085f --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.h @@ -0,0 +1,38 @@ +// 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 + { + static inline constexpr std::string_view s_PinTable_DateAdded_Column = "date_added"; + static inline constexpr std::string_view s_PinTable_Note_Column = "note"; + + // 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); + + // Updates only 1.1 metadata columns for an existing pin. + // Returns a value indicating whether there were any changes. + static bool UpdateMetadataById(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/PinTable_1_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable_1_1.cpp new file mode 100644 index 0000000000..1aa9696152 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable_1_1.cpp @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "PinTable.h" +#include +#include +#include "Microsoft/Schema/IPinningIndex.h" + +namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 +{ + namespace + { + using PinRow = std::tuple, std::optional>; + int64_t GetEpochToStore(const Pinning::Pin& pin) + { + return pin.GetDateAdded().has_value() + ? Utility::ConvertSystemClockToUnixEpoch(*pin.GetDateAdded()) + : Utility::ConvertSystemClockToUnixEpoch(std::chrono::system_clock::now()); + } + + 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; + } + } + + 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 = GetEpochToStore(pin); + + 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 = GetEpochToStore(pin); + + 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; + } + + bool PinTable::UpdateMetadataById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin) + { + SQLite::Builder::StatementBuilder builder; + + builder.Update(s_PinTable_Table_Name).Set() + .Column(s_PinTable_DateAdded_Column).AssignValue(GetEpochToStore(pin)) + .Column(s_PinTable_Note_Column).AssignValue(pin.GetNote()) + .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/PinningIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h new file mode 100644 index 0000000000..57ba50cc7d --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h @@ -0,0 +1,18 @@ +// 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; + SQLite::rowid_t AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) override; + std::pair UpdatePin(SQLite::Connection& connection, const Pinning::Pin& pin) override; + std::optional GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) override; + std::vector GetAllPins(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..436a379eb6 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp @@ -0,0 +1,74 @@ +// 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.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; + } + + SQLite::rowid_t PinningIndexInterface::AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "addpin_v1_1"); + SQLite::rowid_t pinId = Pinning_V1_0::PinningIndexInterface::AddPin(connection, pin); + THROW_HR_IF(E_UNEXPECTED, !Pinning_V1_1::PinTable::UpdateMetadataById(connection, pinId, pin)); + + savepoint.Commit(); + return pinId; + } + + std::pair PinningIndexInterface::UpdatePin(SQLite::Connection& connection, const Pinning::Pin& pin) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "updatepin_v1_1"); + auto [baseUpdated, pinId] = Pinning_V1_0::PinningIndexInterface::UpdatePin(connection, pin); + bool metadataUpdated = PinTable::UpdateMetadataById(connection, pinId, pin); + savepoint.Commit(); + + return { baseUpdated || metadataUpdated, pinId }; + } + + std::optional PinningIndexInterface::GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) + { + auto existingPinId = Pinning_V1_0::PinTable::GetIdByPinKey(connection, pinKey); + + if (!existingPinId) + { + return {}; + } + + return PinTable::GetPinById(connection, existingPinId.value()); + } + + std::vector PinningIndexInterface::GetAllPins(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 6286121fb1..fd92fdeb12 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 bcc54394b6..a52d19b2e8 100644 --- a/src/AppInstallerRepositoryCore/PinningData.cpp +++ b/src/AppInstallerRepositoryCore/PinningData.cpp @@ -93,6 +93,12 @@ namespace AppInstaller::Pinning m_database->RemovePin(pinKey); } + bool PinningData::TryRemovePin(const PinKey& pinKey) + { + THROW_HR_IF(E_NOT_VALID_STATE, !IsDatabaseConnected()); + return m_database->TryRemovePin(pinKey); + } + 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 639801962c..6bf60c25fd 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 0ef543a87a..635835633f 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 a7bcd24791..7f83a3d624 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 2acad68092..4e61ddac9b 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 f44eb93e5b..b4c90c2506 100644 --- a/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs +++ b/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs @@ -162,6 +162,18 @@ internal static class ClassesDefinition [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 9d9daa7cfb..027cce1cf3 100644 --- a/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs +++ b/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs @@ -42,5 +42,7 @@ public WinGetProjectionFactory(IInstanceInitializer instanceInitializer) 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 0a5832dc79..2296b794d2 100644 --- a/src/Microsoft.Management.Deployment/ComClsids.cpp +++ b/src/Microsoft.Management.Deployment/ComClsids.cpp @@ -18,6 +18,7 @@ #include "AddPackageCatalogOptions.h" #include "RemovePackageCatalogOptions.h" #include "EditPackageCatalogOptions.h" +#include "PinPackageOptions.h" #pragma warning( pop ) namespace winrt::Microsoft::Management::Deployment @@ -76,6 +77,10 @@ namespace winrt::Microsoft::Management::Deployment { return __uuidof(winrt::Microsoft::Management::Deployment::implementation::EditPackageCatalogOptions); } + 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 6594a6c054..07d0c72810 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 b04a52f628..dffee89665 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 a2d83da941..cce101c649 100644 --- a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj +++ b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj @@ -171,6 +171,9 @@ + + + @@ -224,6 +227,9 @@ + + + diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index 6470aad0ed..2f3a42d23c 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 5cabd9f6ab..d41324e297 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 30.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 7dce535960..6737af1910 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.idl +++ b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -2,7 +2,7 @@ // Licensed under the MIT License. namespace Microsoft.Management.Deployment { - [contractversion(29)] // For version 1.29 + [contractversion(30)] // For version 1.30 apicontract WindowsPackageManagerContract{}; /// State of the install @@ -1716,8 +1716,130 @@ namespace Microsoft.Management.Deployment /// Edit an existing Windows Package Catalog. EditPackageCatalogResult EditPackageCatalog(EditPackageCatalogOptions options); } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 30)] + { + /// 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, 30)] + 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, 30)] + 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, 30)] + 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, 30)] + 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, 30)] + 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. @@ -1826,5 +1948,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 177c2e1c65..b709392754 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 c2b203e0ed..0be7f672b3 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 a763c6933c..46bbe5fda9 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/FinderPackageCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/FinderPackageCommand.cs @@ -82,6 +82,7 @@ public void Get(string psPackageFieldMatchOption) () => this.FindPackages( CompositeSearchBehavior.LocalCatalogs, PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption))); + for (var i = 0; i < results.Count; i++) { this.Write(StreamType.Object, new PSInstalledCatalogPackage(results[i].CatalogPackage)); 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 f3ed366af5..83d3e6887c 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 de3f0d82b6..cb88793a53 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 ec56a4b6d2..98bf8d6bb0 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 0858537e50..74140b818b 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs @@ -8,19 +8,28 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { using System; using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Engine.Helpers; /// /// InstalledCatalogPackage wrapper object for displaying to PowerShell. /// public sealed class PSInstalledCatalogPackage : PSCatalogPackage { + private readonly Func isPinnedLookup; + private bool? isPinned; + /// /// Initializes a new instance of the class. /// /// The catalog package COM object. - internal PSInstalledCatalogPackage(CatalogPackage catalogPackage) + /// + /// Optional lookup used to determine whether the package is pinned. When not supplied, + /// the lookup is resolved on demand through the package manager. + /// + internal PSInstalledCatalogPackage(CatalogPackage catalogPackage, Func? isPinnedLookup = null) : base(catalogPackage) { + this.isPinnedLookup = isPinnedLookup ?? this.DefaultIsPinnedLookup; } /// @@ -31,6 +40,22 @@ 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.isPinnedLookup(this.CatalogPackageCOM); + } + + return this.isPinned.Value; + } + } + /// /// Compares versions. /// @@ -47,5 +72,15 @@ public PSCompareResult CompareToVersion(string version) _ => throw new InvalidOperationException(), }; } + + /// + /// Determines whether the specified catalog package has one or more pins. + /// + /// The catalog package to inspect. + /// if the package has pins; otherwise, . + private bool DefaultIsPinnedLookup(CatalogPackage catalogPackage) + { + return PackageManagerWrapper.Instance.GetPins(catalogPackage).Count > 0; + } } } 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/Microsoft.WinGet.UnitTests/PSInstalledCatalogPackageTests.cs b/src/PowerShell/Microsoft.WinGet.UnitTests/PSInstalledCatalogPackageTests.cs new file mode 100644 index 0000000000..7f2a353cde --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.UnitTests/PSInstalledCatalogPackageTests.cs @@ -0,0 +1,43 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.UnitTests +{ + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Engine.PSObjects; + using Xunit; + + /// + /// Tests for . + /// + public class PSInstalledCatalogPackageTests + { + /// + /// Tests that IsPinned is computed lazily and cached after the first access. + /// + [Fact] + public void IsPinned_ComputesOnceAndCachesResult() + { + var calls = 0; + CatalogPackage catalogPackage = new (objRef: null!); + PSInstalledCatalogPackage package = new ( + catalogPackage, + _ => + { + calls++; + return true; + }); + + Assert.Equal(0, calls); + + Assert.True(package.IsPinned); + Assert.Equal(1, calls); + + Assert.True(package.IsPinned); + Assert.Equal(1, calls); + } + } +} diff --git a/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 index 9cd9a08564..e2d7eb38d8 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