Files
Dario LipicarandClaude Opus 5 5feeb671ba feat(abi): put dependency constraints on the wire (#64)
* feat(deps): carry dependency constraints and the mismatch status across the ABI

toLogosMap(const InstalledPackage&) is where the range and the signer died.
lgpm carried {name, version, signer} intact from metadata.json through the
.lgx, `lgx verify` and `lgpm install` onto disk; two readers over the SAME
installed tree then disagreed:

  lgpm --json info app                 -> dependencyConstraints:[{...}]
  package_manager.getInstalledPackages -> dependencies:["lib"]

Same library, same files, one serialiser -- this one. Everything past it,
basecamp included, was reading a payload the constraint had been stripped
from.

`dependencyConstraints` is added as a SEPARATE key rather than by widening
`dependencies`, because widening it would fail SILENTLY. basecamp's
PluginLoader::loadCoreDependencies reads that very list (via UIPluginManager's
m_uiPluginMetadata, filled from getInstalledUiPlugins) and does
  QString depName = dep.toString(); if (depName.isEmpty()) continue;
-- a QVariantMap stringifies to empty, so an object-form entry would be
skipped and the dependency simply never loaded, with no diagnostic. The graph
edge and the constraint on that edge are different facts; they now travel in
different keys.

toFlatLogosMap gains the same treatment for the tree APIs: version_mismatch
keeps `version` and `installType` because it resolved to a real installed
package and "needs ^2.0.0, have 1.0.0" takes both numbers, and the per-edge
`requiredVersion` / `requiredSigner` are emitted only when the parent declared
them. Nothing here evaluates the signer.

Both keys are absent whenever every entry is a bare name, which is every
package in the fleet today, so this is backward-compatible by construction.

The unit tests build against tests/stubs/package_manager_lib.h, so the stub
and its mock are updated to mirror the real header -- otherwise the impl
compiles in the integration target and not the mocked one.

Tests 100 -> 107. Red on the base, reading only emitted keys so the probe
needs no field the base lacks:
  ASSERT failed: list[0].contains("dependencyConstraints")

Note: the five integration test files are NOT built by the `unit-tests` check
-- CMake reports "package_manager_lib not found in ../lib -- skipping
integration tests" because the test derivation never stages the external lib.
The end-to-end tests added to tests/test_scanning.cpp are therefore correct
but unexercised in CI; they were driven by hand against the real library.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(deps): carry the observed signer, and the two signer statuses, across the ABI

Two facts have to reach the far side, and they are different facts:

    requiredSigner — a PIN a dependant declared. A demand.
    observedSigner — the DID whose signature over the installed package was
                     VERIFIED at install. An observation.

Everything downstream ("published by a different signer") needs both. The pin
alone does not say what went wrong; the observation alone is an accusation with
no charge attached. `requiredSigner` already crossed and was, in this file's own
words, "carried and compared by nobody" — logos-package-manager now compares it,
so the other half has to arrive too.

New statuses, both about packages that ARE installed:
    signer_mismatch — provably not the package the dependant named
    signer_unknown  — the pin could not be checked; nothing recorded a
                      publisher. Absence of evidence, its own status.

`observedSigner` is additive and emitted only when something was observed, so
an unsigned, embedded or pre-sidecar install crosses byte-identically to
before, and a reader can tell "nothing recorded" (key absent) from a recorded
value. A key present with an empty string would be neither, and would read as
"observed to be unsigned" — which nothing on disk records.

Also replaces `Installed || VersionMismatch` with
nodeResolvedToAnInstalledPackage at the version/installType gate. That chain
named the two statuses that existed when it was written, so the moment these
two were appended it would have started blanking `version` on packages sitting
right there on disk — in this file and in the library's JSON emitter, quietly,
at two call sites in two repositories. The RED run below shows exactly that:
"expected [1.0.0] but got []" for a signer-mismatched dependency.

And the mock's flatten() is brought back in line with the real library, which
PROMOTES on dedup (a later edge judging a package more harshly wins) rather
than merely deduping. The mock had been left behind by that change, so a module
test could have seen a satisfied row for a package the real resolver rejects. A
test double that disagrees with the thing it doubles is worse than none.

RED (observedSigner dropped from both emitters, gate back to the two-status
chain): 109 passed, 4 failed
    getInstalledPackages_carries_the_observed_signer
    resolveDependencies_surfaces_signer_mismatch_with_both_dids
    resolveFlatDependencies_surfaces_signer_mismatch
    resolveDependencies_surfaces_signer_unknown_without_an_observed_did
      ASSERT_EQ failed: expected [1.0.0] but got []

GREEN: 113 passed.

Both runs use --override-input logos-package-manager path:<local checkout>:
this branch's flake.lock is still pinned at the parent branch's mid-history
commit and gets relocked when the parent merges, per the stack's merge order.
    checks.x86_64-linux.unit-tests
    -> /nix/store/rd7lrab96jvi9n3ycjx2h8czhs9j91a3-logos-package_manager-tests-1.0.0
    packages.x86_64-linux.lib
    -> /nix/store/hlnjk6rrz73q6hjcsn4c3vr14y57liis-logos-package_manager-module-lib-1.0.0
    packages.x86_64-linux.default
    -> /nix/store/wvlj38yw066icjypvppzncjpc442ympr-logos-package_manager-module

That .so was then staged into a real installed tree and driven through a real
logoscore daemon; the five wire payloads are in the branch summary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(deps): follow the rename of observedSigner to signerDid

logos-package-manager stopped recording who it verified at install and started
carrying the package's own manifest.sig into the install tree, so the value
that crosses this ABI changed meaning and changed name with it.

`observedSigner` was an observation the installer made and wrote down.
`signerDid` is what the installed signature says about itself, reported only
once that signature has been checked against the key its own DID carries. The
difference matters to a reader on the far side: it is not corroborated by
anything outside the document, so it is for display, and the verdict beside it
is NOT these two values compared — it comes from verifying the installed
signature under the PIN's key. A `signer_mismatch` row therefore carries a
`signerDid` that differs from `requiredSigner`, and that is the normal shape of
that row rather than an inconsistency.

Wire key renamed to match, in both the installed-packages map and the
dependency-tree node. Still additive and still absent when there is no usable
signature, so an unsigned or embedded package crosses the ABI byte-identically
to before.

The test stub mirrors the real struct and moves with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(deps): build against the library this module needs, and pin the wire shape exactly

CI here runs `nix build -L` — the real module, linking the real
logos-package-manager. The lock pinned 7c5aad9a (2026-08-22), whose
DependencyTreeNode has neither `requiredSigner` nor `signerDid`, so that build
does not compile:

  error: no member named requiredSigner in const struct DependencyTreeNode
  error: no member named signerDid      in const struct DependencyTreeNode

That is a red build, not a latent risk. And the one check would never have said
so: `tests.mockCLibs = ["logos_pm"]` builds the unit tests against
tests/stubs/package_manager_lib.h, a hand-maintained MIRROR of the real header.
It compiles and passes against a library it never links, so the check is green
while `nix build` is red and nothing connects the two. flake.nix now pins the
branch carrying those fields, with a REVERT-ON-MERGE note saying exactly this.
Measured: `nix build` and `nix build .#unit-tests` both pass with NO override.

The backward-compatibility test moves from three absences to an EXACT KEY SET.
Naming three keys only rules out the three somebody thought of, and the way an
additive key fails is by turning out not to be conditional. `signerDid` is that
shape: it is a property of the PACKAGE, not of the edge, so it does not become
absent because an edge declared no constraint. A real capture of a satisfied
pin carries `status:"installed"` and a `signerDid` together — so a test that
only looked for requiredVersion/requiredSigner would not have noticed it appear
on an unconstrained node. Counting and naming every key does. Proven by
mutation: one unconditional extra key turns the test red.

Stub and test prose follow the mechanism. What an embedded package is missing
is a manifest.sig, and it can never have one because nothing but
installPluginFile copies one into an install tree — not a record nobody wrote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(deps): cut this PR's comments back to what a reader needs

217 added comment lines -> 132. Comments only; no code line moved or changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 12:00:39 -03:00

1335 lines
48 KiB
C++

#include "package_manager_impl.h"
#include <package_manager_lib.h>
#include <lgx.h>
#include <algorithm>
#include <chrono>
#include <filesystem>
#include <iostream>
#include <limits>
#include <set>
// ---------------------------------------------------------------------------
// Struct → LogosMap / LogosList conversion helpers
// ---------------------------------------------------------------------------
//
// Wire format is identical to what PackageManagerLib / the lgpm CLI emit via
// package_manager_json.cpp's nlohmann ADL hooks. We re-hand-roll here (rather
// than reuse those hooks) so the unit tests can compile against the stub
// header in tests/stubs/package_manager_lib.h without pulling in the lib's
// JSON module.
//
// Keep these in sync with package_manager_json.cpp's to_json definitions.
// ---------------------------------------------------------------------------
namespace {
LogosMap toLogosMap(const Hashes& h)
{
LogosMap m = LogosMap::object();
m["root"] = h.root;
return m;
}
LogosMap toLogosMap(const InstalledPackage& p)
{
LogosMap m = LogosMap::object();
m["name"] = p.name;
m["displayName"] = p.displayName;
m["version"] = p.version;
m["description"] = p.description;
m["type"] = p.type;
m["category"] = p.category;
m["author"] = p.author;
m["license"] = p.license;
m["icon"] = p.icon;
m["manifestVersion"] = p.manifestVersion;
m["view"] = p.view;
LogosList deps = LogosList::array();
for (const auto& d : p.dependencies) deps.push_back(d);
m["dependencies"] = deps;
// A SEPARATE key rather than a widened `dependencies`, which stays an
// array of plain name STRINGS: basecamp's PluginLoader reads that list as
// `dep.toString()` and skips empties, so an object-form entry would
// stringify to empty and the dependency would silently never load. Absent
// when every entry is a bare name, so an unconstrained package crosses
// byte-identically. Mirrors package_manager_json.cpp's
// `dependencyConstraints`. These are pins on OTHERS; `signerDid` below is
// what this package claims about itself.
if (!p.dependencyConstraints.empty()) {
LogosList constraints = LogosList::array();
for (const auto& d : p.dependencyConstraints) {
LogosMap c = LogosMap::object();
c["name"] = d.name;
if (d.version) c["version"] = *d.version;
if (d.signer) c["signer"] = *d.signer;
constraints.push_back(c);
}
m["dependencyConstraints"] = constraints;
}
// The DID the installed manifest.sig names, emitted only once that
// signature verified under the key the DID itself carries. That is
// self-consistency, not identity; only `requiredSigner` settles identity.
// Absent when no usable signature is installed, so an unsigned or embedded
// package crosses byte-identically; an empty string would be ambiguous.
// Deliberately the same key name as in the installPluginFile /
// inspectPackage / verifySignature responses, but those come from
// lgx_verify_signature, which fills signer_did BEFORE checking, so they
// need a companion signatureStatus key and this one does not.
if (p.signerDid) m["signerDid"] = *p.signerDid;
m["hashes"] = toLogosMap(p.hashes);
m["installType"] = std::string(installTypeToString(p.installType));
m["installDir"] = p.installDir;
m["mainFilePath"] = p.mainFilePath;
return m;
}
LogosList toLogosList(const std::vector<InstalledPackage>& v)
{
LogosList out = LogosList::array();
for (const auto& p : v) out.push_back(toLogosMap(p));
return out;
}
// Flat per-node projection — just the node's own fields, no `children`.
// Shared between the flat list APIs (resolveFlatDependencies /
// resolveFlatDependents) and the tree APIs (where each recursive step is
// "this node's fields plus its children").
LogosMap toFlatLogosMap(const DependencyTreeNode& n)
{
LogosMap m = LogosMap::object();
m["name"] = n.name;
m["status"] = std::string(dependencyStatusToString(n.status));
// Every status except NotInstalled and Cycle resolved to a package that IS
// installed, so it carries the fields Installed does — "needs ^2.0.0, have
// 1.0.0" is only actionable with both numbers. A predicate, not an
// `Installed || VersionMismatch` chain: such a chain silently starts
// blanking the version on each status appended after it was written.
if (nodeResolvedToAnInstalledPackage(n.status)) {
m["version"] = n.version;
m["installType"] = std::string(installTypeToString(n.installType));
} else {
m["version"] = "";
m["installType"] = "";
}
// The constraint the parent edge declared; absent for an unconstrained
// edge, so a bare-name tree crosses byte-identically to before.
// `requiredSigner` is judged by verifying the installed signature under the
// PIN's own key, not by comparing it to `signerDid`.
if (n.requiredVersion) m["requiredVersion"] = *n.requiredVersion;
if (n.requiredSigner) m["requiredSigner"] = *n.requiredSigner;
// What the installed package's own signature says about itself; absent
// when none is installed, which is what makes a `signer_unknown` row
// legible without a second call. A `signer_mismatch` row carries both, and
// the two differing is the normal shape of that row.
if (n.signerDid) m["signerDid"] = *n.signerDid;
return m;
}
LogosMap toFlatLogosMap(const DependentTreeNode& n)
{
LogosMap m = LogosMap::object();
m["name"] = n.name;
m["version"] = n.version;
m["type"] = n.type;
m["installType"] = std::string(installTypeToString(n.installType));
m["installDir"] = n.installDir;
return m;
}
// Depth-clipped tree serialisation — root always emitted with its own
// fields; `maxDepth` bounds how far we recurse into `children`. Used by
// resolveDependencies/resolveDependents: maxDepth=1 for !recursive (root
// + direct children with empty children arrays), maxDepth=INT_MAX for
// the full tree.
template <typename Node>
LogosMap toLogosTreeMap(const Node& n, int maxDepth)
{
LogosMap m = toFlatLogosMap(n);
LogosList children = LogosList::array();
if (maxDepth > 0) {
for (const auto& c : n.children)
children.push_back(toLogosTreeMap(c, maxDepth - 1));
}
m["children"] = children;
return m;
}
template <typename Node>
LogosList toFlatLogosList(const std::vector<Node>& v)
{
LogosList out = LogosList::array();
for (const auto& n : v) out.push_back(toFlatLogosMap(n));
return out;
}
} // namespace
PackageManagerImpl::PackageManagerImpl()
: m_lib(nullptr)
{
m_lib = new PackageManagerLib();
}
PackageManagerImpl::~PackageManagerImpl()
{
// Signal any running worker thread to exit, then join it before tearing
// down state it might still reference (m_pendingAction, read when the
// worker emits its cancellation event via the typed event methods). The
// lock is taken briefly to publish m_ackShutdown and bump m_ackGeneration
// atomically; notify + join happen outside the lock so the worker can
// re-acquire and exit its wait_for.
{
std::lock_guard<std::mutex> lk(m_stateMutex);
m_ackShutdown = true;
++m_ackGeneration;
}
m_ackCv.notify_all();
if (m_ackThread.joinable()) m_ackThread.join();
delete m_lib;
m_lib = nullptr;
}
LogosMap PackageManagerImpl::installPlugin(const std::string& pluginPath, bool skipIfNotNewerVersion)
{
std::string errorMsg;
std::string installedPluginPath;
bool isCoreModule = false;
std::string result = m_lib->installPluginFile(
pluginPath, errorMsg, skipIfNotNewerVersion,
&installedPluginPath, &isCoreModule
);
// The library reports success by returning a non-empty install location.
// installedPluginPath is a REPORTING detail, not the success signal: a
// QML-only ui_qml package ("main": {}) has no backend library, so it used
// to come back empty from a perfectly good install. Gating on it here had
// two consequences — uiPluginFileInstalled never fired (so Basecamp only
// discovered the plugin after a restart) and response["path"] was empty,
// which logos-package-manager-ui reads as failure and renders as a red
// RETRY. Patched libraries always fill installedPluginPath in; the `result`
// fallback keeps this correct against an older one.
bool success = !result.empty();
const std::string reportedPath = installedPluginPath.empty() ? result : installedPluginPath;
if (success) {
if (isCoreModule) {
corePluginFileInstalled(reportedPath);
} else {
uiPluginFileInstalled(reportedPath);
}
}
// Get signature info for the response
auto sigResult = m_lib->verifyPackageSignature(pluginPath);
std::string stem = std::filesystem::path(pluginPath).stem().string();
LogosMap response;
response["name"] = stem;
response["path"] = success ? reportedPath : std::string();
response["isCoreModule"] = isCoreModule;
if (!success) {
response["error"] = errorMsg;
}
// Add signature info
if (sigResult.is_signed) {
bool valid = sigResult.signature_valid && sigResult.package_valid;
response["signatureStatus"] = valid ? std::string("signed") : std::string("invalid");
response["signerDid"] = sigResult.signer_did;
if (!sigResult.signer_name.empty())
response["signerName"] = sigResult.signer_name;
if (!sigResult.signer_url.empty())
response["signerUrl"] = sigResult.signer_url;
if (!sigResult.trusted_as.empty())
response["trustedAs"] = sigResult.trusted_as;
} else if (!sigResult.error.empty()) {
response["signatureStatus"] = std::string("error");
response["signatureError"] = sigResult.error;
} else {
response["signatureStatus"] = std::string("unsigned");
}
return response;
}
LogosMap PackageManagerImpl::inspectPackage(const std::string& lgxPath)
{
LogosMap result;
lgx_package_t pkg = lgx_load(lgxPath.c_str());
if (!pkg) {
result["error"] = std::string("Failed to load LGX package: ")
+ (lgx_get_last_error() ? lgx_get_last_error() : "unknown");
return result;
}
const char* rawName = lgx_get_name(pkg);
const char* rawVersion = lgx_get_version(pkg);
const char* rawDesc = lgx_get_description(pkg);
const char* rawManifest = lgx_get_manifest_json(pkg);
std::string pkgName = rawName ? rawName : "";
std::string pkgVersion = rawVersion ? rawVersion : "";
result["name"] = pkgName;
result["version"] = pkgVersion;
result["description"] = rawDesc ? std::string(rawDesc) : "";
// Extract type, category, and root content hash from the embedded
// manifest. The root hash (Merkle tree root over the package content,
// `manifest.hashes.root`) is the same identifier PMU renders when
// browsing the online catalog — surfacing it here lets the install
// confirmation dialog show a stable per-release fingerprint.
if (rawManifest) {
try {
auto doc = LogosMap::parse(rawManifest);
result["type"] = doc.value("type", "");
result["category"] = doc.value("category", "");
if (doc.contains("hashes") && doc["hashes"].is_object()) {
result["rootHash"] = doc["hashes"].value("root", "");
} else {
result["rootHash"] = "";
}
} catch (...) {
result["type"] = "";
result["category"] = "";
result["rootHash"] = "";
}
}
// Available platform variants.
const char** variants = lgx_get_variants(pkg);
LogosList variantList = LogosList::array();
if (variants) {
for (int i = 0; variants[i]; ++i)
variantList.push_back(std::string(variants[i]));
lgx_free_string_array(variants);
}
result["variants"] = variantList;
lgx_free_package(pkg);
// Signature verification — standalone, no install side effects.
auto sig = m_lib->verifyPackageSignature(lgxPath);
if (sig.is_signed) {
bool valid = sig.signature_valid && sig.package_valid;
result["signatureStatus"] = valid ? std::string("signed")
: std::string("invalid");
result["signerDid"] = sig.signer_did;
result["signerName"] = sig.signer_name;
} else if (!sig.error.empty()) {
result["signatureStatus"] = std::string("error");
} else {
result["signatureStatus"] = std::string("unsigned");
}
// Check if this package is already installed.
bool isAlreadyInstalled = false;
std::string installedVersion;
std::string installedHash;
std::vector<InstalledPackage> scan = m_lib->getInstalledPackages();
for (const auto& entry : scan) {
if (entry.name == pkgName) {
isAlreadyInstalled = true;
installedVersion = entry.version;
// Passthrough from the installed manifest.json; same field PMU
// reads in the online catalog (`manifest.hashes.root`).
installedHash = entry.hashes.root;
break;
}
}
result["isAlreadyInstalled"] = isAlreadyInstalled;
result["installedVersion"] = installedVersion;
result["installedHash"] = installedHash;
// If already installed, compute reverse dependents so the dialog can
// show what would be affected by an upgrade.
if (isAlreadyInstalled) {
auto deps = installedDependentsNames(pkgName);
LogosList depList = LogosList::array();
for (const auto& d : deps) depList.push_back(d);
result["installedDependents"] = depList;
}
return result;
}
LogosList PackageManagerImpl::getInstalledPackages()
{
return toLogosList(m_lib->getInstalledPackages());
}
LogosList PackageManagerImpl::getInstalledModules()
{
return toLogosList(m_lib->getInstalledModules());
}
LogosList PackageManagerImpl::getInstalledUiPlugins()
{
return toLogosList(m_lib->getInstalledUiPlugins());
}
LogosMap PackageManagerImpl::uninstallPackage(const std::string& packageName)
{
return doUninstall(packageName);
}
LogosMap PackageManagerImpl::doUninstall(const std::string& packageName)
{
// Inspect the package before removal so we know whether to emit a core or UI event.
std::vector<InstalledPackage> scan = m_lib->getInstalledPackages();
std::string moduleType;
for (const auto& entry : scan) {
if (entry.name == packageName) {
moduleType = entry.type;
break;
}
}
UninstallResult r = m_lib->uninstallPackage(packageName);
LogosMap response;
response["success"] = r.success;
if (!r.success) {
response["error"] = r.errorMsg;
} else {
LogosList removed = LogosList::array();
for (const auto& f : r.removedFiles) removed.push_back(f);
response["removedFiles"] = removed;
if (moduleType == "core") {
corePluginUninstalled(packageName);
} else {
uiPluginUninstalled(packageName);
}
}
return response;
}
LogosMap PackageManagerImpl::resolveDependencies(const std::string& packageName, bool recursive)
{
// Unknown roots surface as nullopt from the library; keep an empty
// object on the wire so callers can `.contains(...)` without branching.
auto tree = m_lib->resolveDependencies(packageName);
if (!tree) return LogosMap::object();
// maxDepth=1 clips to root + direct children (children with empty
// `children` arrays); INT_MAX walks the full tree.
return toLogosTreeMap(*tree, recursive ? std::numeric_limits<int>::max() : 1);
}
LogosMap PackageManagerImpl::resolveDependents(const std::string& packageName, bool recursive)
{
// Same shape treatment as resolveDependencies — the library returns a
// tree, we either clip it at depth 1 or walk the full reverse subtree.
auto tree = m_lib->resolveDependents(packageName);
if (!tree) return LogosMap::object();
return toLogosTreeMap(*tree, recursive ? std::numeric_limits<int>::max() : 1);
}
LogosList PackageManagerImpl::resolveFlatDependencies(const std::string& packageName, bool recursive)
{
// Flat list of per-node maps (no `children`). recursive=false emits
// only the root's direct children; recursive=true emits every
// descendant, BFS-ordered and deduped by name (via DependencyTreeNode::flatten()).
auto tree = m_lib->resolveDependencies(packageName);
if (!tree) return LogosList::array();
return recursive ? toFlatLogosList(tree->flatten())
: toFlatLogosList(tree->children);
}
LogosList PackageManagerImpl::resolveFlatDependents(const std::string& packageName, bool recursive)
{
auto tree = m_lib->resolveDependents(packageName);
if (!tree) return LogosList::array();
return recursive ? toFlatLogosList(tree->flatten())
: toFlatLogosList(tree->children);
}
std::vector<std::string> PackageManagerImpl::getValidVariants()
{
return PackageManagerLib::platformVariantsToTry();
}
void PackageManagerImpl::setEmbeddedModulesDirectory(const std::string& dir)
{
m_lib->setEmbeddedModulesDirectory(dir);
}
void PackageManagerImpl::addEmbeddedModulesDirectory(const std::string& dir)
{
m_lib->addEmbeddedModulesDirectory(dir);
}
void PackageManagerImpl::setEmbeddedUiPluginsDirectory(const std::string& dir)
{
m_lib->setEmbeddedUiPluginsDirectory(dir);
}
void PackageManagerImpl::addEmbeddedUiPluginsDirectory(const std::string& dir)
{
m_lib->addEmbeddedUiPluginsDirectory(dir);
}
void PackageManagerImpl::setUserModulesDirectory(const std::string& dir)
{
m_lib->setUserModulesDirectory(dir);
}
void PackageManagerImpl::setUserUiPluginsDirectory(const std::string& dir)
{
m_lib->setUserUiPluginsDirectory(dir);
}
void PackageManagerImpl::setSignaturePolicy(const std::string& policy)
{
std::string p = policy;
std::transform(p.begin(), p.end(), p.begin(), ::tolower);
if (p == "none") m_lib->setSignaturePolicy(SignaturePolicy::NONE);
else if (p == "warn") m_lib->setSignaturePolicy(SignaturePolicy::WARN);
else if (p == "require") m_lib->setSignaturePolicy(SignaturePolicy::REQUIRE);
else {
std::cerr << "PackageManagerImpl::setSignaturePolicy: invalid policy '"
<< policy << "' - expected one of: none, warn, require\n";
}
}
void PackageManagerImpl::setKeyringDirectory(const std::string& dir)
{
m_lib->setKeyringDirectory(dir);
}
LogosMap PackageManagerImpl::verifyPackage(const std::string& lgxPath)
{
auto result = m_lib->verifyPackageSignature(lgxPath);
LogosMap response;
response["isSigned"] = result.is_signed;
response["signatureValid"] = result.signature_valid;
response["packageValid"] = result.package_valid;
response["signerDid"] = result.signer_did;
response["signerName"] = result.signer_name;
response["signerUrl"] = result.signer_url;
response["trustedAs"] = result.trusted_as;
if (!result.error.empty())
response["error"] = result.error;
return response;
}
LogosMap PackageManagerImpl::addTrustedKey(const std::string& name, const std::string& did,
const std::string& displayName, const std::string& url)
{
std::string keyringDir = m_lib->keyringDirectory();
const char* keyringDirPtr = keyringDir.empty() ? nullptr : keyringDir.c_str();
lgx_result_t res = lgx_keyring_add(
keyringDirPtr,
name.c_str(),
did.c_str(),
displayName.empty() ? nullptr : displayName.c_str(),
url.empty() ? nullptr : url.c_str()
);
LogosMap response;
response["success"] = static_cast<bool>(res.success);
if (!res.success && res.error)
response["error"] = std::string(res.error);
return response;
}
LogosMap PackageManagerImpl::removeTrustedKey(const std::string& name)
{
std::string keyringDir = m_lib->keyringDirectory();
const char* keyringDirPtr = keyringDir.empty() ? nullptr : keyringDir.c_str();
lgx_result_t res = lgx_keyring_remove(
keyringDirPtr,
name.c_str()
);
LogosMap response;
response["success"] = static_cast<bool>(res.success);
if (!res.success && res.error)
response["error"] = std::string(res.error);
return response;
}
LogosList PackageManagerImpl::listTrustedKeys()
{
std::string keyringDir = m_lib->keyringDirectory();
const char* keyringDirPtr = keyringDir.empty() ? nullptr : keyringDir.c_str();
lgx_keyring_list_t list = lgx_keyring_list(keyringDirPtr);
LogosList result = LogosList::array();
for (size_t i = 0; i < list.count; ++i) {
LogosMap entry;
if (list.keys[i].name) entry["name"] = std::string(list.keys[i].name);
if (list.keys[i].did) entry["did"] = std::string(list.keys[i].did);
if (list.keys[i].display_name) entry["displayName"] = std::string(list.keys[i].display_name);
if (list.keys[i].url) entry["url"] = std::string(list.keys[i].url);
if (list.keys[i].added_at) entry["addedAt"] = std::string(list.keys[i].added_at);
result.push_back(entry);
}
lgx_free_keyring_list(list);
return result;
}
// ---------------------------------------------------------------------------
// Gated uninstall / upgrade flow
// ---------------------------------------------------------------------------
const char* PackageManagerImpl::opName(PendingOp op)
{
switch (op) {
case PendingOp::Uninstall: return "uninstall";
case PendingOp::Upgrade: return "upgrade";
case PendingOp::Install: return "install";
case PendingOp::MultiUninstall: return "multi-uninstall";
case PendingOp::None: return "none";
}
return "none";
}
// Human-readable description of the pending action for cross-op blocking
// error messages. Single-name ops include the package name; multi includes
// the batch size (showing names[0] alone would be misleading for a batch).
// Caller must hold m_stateMutex.
std::string PackageManagerImpl::pendingDescriptionLocked() const
{
std::string desc = std::string("Another ") + opName(m_pendingAction.op);
if (m_pendingAction.op == PendingOp::MultiUninstall) {
desc += " is in progress (batch of "
+ std::to_string(m_pendingAction.names.size()) + " packages)";
} else {
desc += " is in progress for '" + m_pendingAction.name + "'";
}
return desc;
}
bool PackageManagerImpl::isEmbedded(const std::string& packageName) const
{
std::vector<InstalledPackage> scan = m_lib->getInstalledPackages();
for (const auto& entry : scan) {
if (entry.name == packageName)
return entry.installType == InstallType::Embedded;
}
return false;
}
std::vector<std::string> PackageManagerImpl::installedDependentsNames(const std::string& packageName) const
{
std::vector<std::string> names;
auto tree = m_lib->resolveDependents(packageName);
if (!tree) return names;
auto flat = tree->flatten();
names.reserve(flat.size());
for (const auto& d : flat) {
if (!d.name.empty()) names.push_back(d.name);
}
return names;
}
// ---------------------------------------------------------------------------
// Pure-C++ ack timer — std::thread + std::condition_variable replacing QTimer.
// See detailed protocol comment in the header.
// ---------------------------------------------------------------------------
void PackageManagerImpl::startAckTimerLocked(std::unique_lock<std::mutex>& lock)
{
// Precondition: caller holds m_stateMutex via `lock`.
// Bump the generation and wake any previously-running worker. If one is
// still waiting on the CV, it'll re-acquire the mutex, see its captured
// generation is stale, and bail.
++m_ackGeneration;
m_ackCv.notify_all();
// Join the previous worker (if any) before replacing m_ackThread —
// assigning to a joinable std::thread is undefined behaviour. Release
// the lock during join so the worker can proceed past its wait_for;
// otherwise we'd deadlock (we hold the lock the worker needs).
if (m_ackThread.joinable()) {
lock.unlock();
m_ackThread.join();
lock.lock();
}
const uint64_t gen = m_ackGeneration;
m_ackThread = std::thread([this, gen]() { ackTimerWorker(gen); });
}
void PackageManagerImpl::stopAckTimerLocked()
{
// Caller holds m_stateMutex. Bump the generation and notify so any
// running worker wakes up and exits silently. Do NOT join here: the
// slot calling us is likely running on the module thread and the
// worker might be mid-wait needing the mutex we hold. The worker
// will exit on its own; the next startAckTimerLocked (or the
// destructor) reaps the std::thread handle.
++m_ackGeneration;
m_ackCv.notify_all();
}
void PackageManagerImpl::ackTimerWorker(uint64_t myGeneration)
{
std::unique_lock<std::mutex> lock(m_stateMutex);
// wait_for returns true when the predicate is satisfied, false on
// timeout. Predicate: "stop waiting" — either the process is shutting
// down or our generation is stale (a newer request / ack / cancel
// has superseded us).
bool cancelled = m_ackCv.wait_for(
lock,
std::chrono::milliseconds(m_ackTimeoutMs),
[this, myGeneration]() {
return m_ackShutdown || m_ackGeneration != myGeneration;
}
);
if (cancelled) return;
// Full timeout with no cancellation — but recheck state now that we
// hold the lock. An ack or a different state change could have
// landed between the last CV check and here (unlikely, but cheap to
// verify).
if (m_ackShutdown) return;
if (m_ackGeneration != myGeneration) return;
if (m_pendingAction.op == PendingOp::None || m_pendingAction.acked) return;
// Claim the pending action so slot-side code sees a clean slate.
PendingAction pa = m_pendingAction;
m_pendingAction = {};
const std::string reason = "no listener acknowledged within "
+ std::to_string(m_ackTimeoutMs) + "ms";
// Release the lock before emitting — event emission marshals through a
// Qt signal; a listener synchronously calling back into this impl
// (e.g. a headless runtime that calls uninstallPackage on cancel
// notification) would otherwise re-enter the mutex and deadlock.
lock.unlock();
emitCancellation(pa, reason);
}
void PackageManagerImpl::emitCancellation(const PendingAction& pa, const std::string& reason)
{
LogosMap payload;
payload["reason"] = reason;
if (pa.op == PendingOp::Upgrade) {
payload["name"] = pa.name;
payload["releaseTag"] = pa.releaseTag;
upgradeCancelled(payload.dump());
} else if (pa.op == PendingOp::Uninstall) {
payload["name"] = pa.name;
uninstallCancelled(payload.dump());
} else if (pa.op == PendingOp::Install) {
payload["name"] = pa.name;
payload["releaseTag"] = pa.releaseTag;
payload["repositoryUrl"] = pa.repositoryUrl;
installCancelled(payload.dump());
} else if (pa.op == PendingOp::MultiUninstall) {
LogosList names = LogosList::array();
for (const auto& n : pa.names) names.push_back(n);
payload["names"] = names;
multiUninstallCancelled(payload.dump());
}
}
LogosMap PackageManagerImpl::requestUninstall(const std::string& packageName)
{
LogosMap response;
// Empty packageName would be persisted into m_pendingAction.name and then
// broadcast via beforeUninstall(payload{name:""}), causing listeners to
// open a dialog titled "Uninstall ''?" with no dependents. Reject early
// with a distinct error so callers can surface a sane toast and callers
// that ARE the GUI can avoid showing a stray dialog.
if (packageName.empty()) {
response["success"] = false;
response["error"] = "Package name cannot be empty";
return response;
}
std::unique_lock<std::mutex> lock(m_stateMutex);
if (m_pendingAction.op != PendingOp::None) {
response["success"] = false;
response["error"] = pendingDescriptionLocked();
return response;
}
if (isEmbedded(packageName)) {
response["success"] = false;
response["error"] = "Cannot uninstall embedded module '" + packageName + "'";
return response;
}
m_pendingAction = {};
m_pendingAction.op = PendingOp::Uninstall;
m_pendingAction.name = packageName;
m_pendingAction.acked = false;
// Build the event payload while we still hold the lock (so m_lib reads
// don't race against a concurrent slot). The event emission itself is
// deferred until after the unlock — see the reentrancy note in ackTimerWorker.
LogosMap payload;
payload["name"] = packageName;
LogosList deps = LogosList::array();
for (const auto& d : installedDependentsNames(packageName))
deps.push_back(d);
payload["installedDependents"] = deps;
// Start the ack timer (this may briefly release + re-acquire `lock`
// while joining a previous worker).
startAckTimerLocked(lock);
lock.unlock();
beforeUninstall(payload.dump());
response["success"] = true;
return response;
}
// Parse the initiator-supplied depChanges JSON (array of change records) and
// attach it to a gated-flow event payload under "depChanges". The module never
// interprets it — it's opaque display data for the host's confirmation dialog —
// so a malformed / empty string simply yields an empty array rather than an
// error (the dialog then shows "no other packages need to change").
static void attachDepChanges(LogosMap& payload, const std::string& depChanges)
{
LogosList changes = LogosList::array();
if (!depChanges.empty()) {
try {
LogosMap parsed = LogosMap::parse(depChanges);
if (parsed.is_array())
changes = std::move(parsed);
} catch (...) {
// leave `changes` empty on any parse failure
}
}
payload["depChanges"] = changes;
}
LogosMap PackageManagerImpl::requestUpgrade(const std::string& packageName,
const std::string& releaseTag,
int64_t mode,
const std::string& depChanges)
{
LogosMap response;
// Same rationale as requestUninstall: empty name has to be rejected
// before we set pending state, otherwise beforeUpgrade(name="") leads
// listeners into an empty-title dialog.
if (packageName.empty()) {
response["success"] = false;
response["error"] = "Package name cannot be empty";
return response;
}
std::unique_lock<std::mutex> lock(m_stateMutex);
if (m_pendingAction.op != PendingOp::None) {
response["success"] = false;
response["error"] = pendingDescriptionLocked();
return response;
}
if (isEmbedded(packageName)) {
response["success"] = false;
response["error"] = "Cannot upgrade embedded module '" + packageName + "'";
return response;
}
m_pendingAction = {};
m_pendingAction.op = PendingOp::Upgrade;
m_pendingAction.name = packageName;
m_pendingAction.releaseTag = releaseTag;
m_pendingAction.mode = mode;
m_pendingAction.acked = false;
LogosMap payload;
payload["name"] = packageName;
payload["releaseTag"] = releaseTag;
payload["mode"] = mode;
LogosList deps = LogosList::array();
for (const auto& d : installedDependentsNames(packageName))
deps.push_back(d);
payload["installedDependents"] = deps;
attachDepChanges(payload, depChanges);
startAckTimerLocked(lock);
lock.unlock();
beforeUpgrade(payload.dump());
response["success"] = true;
return response;
}
LogosMap PackageManagerImpl::requestInstall(const std::string& packageName,
const std::string& releaseTag,
const std::string& repositoryUrl,
const std::string& depChanges)
{
LogosMap response;
if (packageName.empty()) {
response["success"] = false;
response["error"] = "Package name cannot be empty";
return response;
}
std::unique_lock<std::mutex> lock(m_stateMutex);
if (m_pendingAction.op != PendingOp::None) {
response["success"] = false;
response["error"] = pendingDescriptionLocked();
return response;
}
m_pendingAction = {};
m_pendingAction.op = PendingOp::Install;
m_pendingAction.name = packageName;
m_pendingAction.releaseTag = releaseTag;
m_pendingAction.repositoryUrl = repositoryUrl;
m_pendingAction.acked = false;
LogosMap payload;
payload["name"] = packageName;
payload["releaseTag"] = releaseTag;
payload["repositoryUrl"] = repositoryUrl;
attachDepChanges(payload, depChanges);
startAckTimerLocked(lock);
lock.unlock();
beforeInstall(payload.dump());
response["success"] = true;
return response;
}
LogosMap PackageManagerImpl::ackPendingAction(const std::string& packageName)
{
std::lock_guard<std::mutex> lock(m_stateMutex);
LogosMap response;
bool match = false;
if (m_pendingAction.op == PendingOp::MultiUninstall) {
match = std::find(m_pendingAction.names.begin(),
m_pendingAction.names.end(),
packageName) != m_pendingAction.names.end();
} else if (m_pendingAction.op != PendingOp::None) {
match = (m_pendingAction.name == packageName);
}
if (!match) {
response["success"] = false;
response["error"] = "No matching pending action to ack for '" + packageName + "'";
return response;
}
// Idempotent — re-acking an already-acked request is a no-op.
m_pendingAction.acked = true;
stopAckTimerLocked();
response["success"] = true;
return response;
}
LogosMap PackageManagerImpl::confirmUninstall(const std::string& packageName)
{
{
std::lock_guard<std::mutex> lock(m_stateMutex);
if (m_pendingAction.op != PendingOp::Uninstall || m_pendingAction.name != packageName) {
LogosMap response;
response["success"] = false;
response["error"] = "No matching pending uninstall for '" + packageName + "'";
return response;
}
if (!m_pendingAction.acked) {
LogosMap response;
response["success"] = false;
response["error"] = "Pending uninstall for '" + packageName + "' has not been acknowledged";
return response;
}
m_pendingAction = {};
stopAckTimerLocked();
}
// Lock released before doUninstall — it emits corePluginUninstalled /
// uiPluginUninstalled, and listeners may synchronously call back into
// this impl (the whole point of the event is to trigger cleanup).
return doUninstall(packageName);
}
LogosMap PackageManagerImpl::cancelUninstall(const std::string& packageName)
{
PendingAction pa;
{
std::lock_guard<std::mutex> lock(m_stateMutex);
if (m_pendingAction.op != PendingOp::Uninstall || m_pendingAction.name != packageName) {
LogosMap response;
response["success"] = false;
response["error"] = "No matching pending uninstall for '" + packageName + "'";
return response;
}
// Symmetric with confirmUninstall: the gated protocol requires the
// owning listener to ack before driving the decision either way.
// An un-acked pending state is owned by the ack-reception timer;
// letting cancel short-circuit it would bypass the protocol and
// suppress the "no listener acknowledged" timeout event that
// initiators otherwise rely on.
if (!m_pendingAction.acked) {
LogosMap response;
response["success"] = false;
response["error"] = "Pending uninstall for '" + packageName + "' has not been acknowledged";
return response;
}
pa = m_pendingAction;
m_pendingAction = {};
stopAckTimerLocked();
}
// Uniform cancellation notification — same event the ack-timeout path emits.
// Initiators (PMU) subscribe once and handle every cancellation consistently.
emitCancellation(pa, "user cancelled");
LogosMap response;
response["success"] = true;
return response;
}
LogosMap PackageManagerImpl::confirmUpgrade(const std::string& packageName,
const std::string& releaseTag)
{
int64_t mode = 0;
{
std::lock_guard<std::mutex> lock(m_stateMutex);
if (m_pendingAction.op != PendingOp::Upgrade
|| m_pendingAction.name != packageName
|| m_pendingAction.releaseTag != releaseTag) {
LogosMap response;
response["success"] = false;
response["error"] = "No matching pending upgrade for '" + packageName + "'";
return response;
}
if (!m_pendingAction.acked) {
LogosMap response;
response["success"] = false;
response["error"] = "Pending upgrade for '" + packageName + "' has not been acknowledged";
return response;
}
mode = m_pendingAction.mode;
m_pendingAction = {};
stopAckTimerLocked();
}
LogosMap uninstallResult = doUninstall(packageName);
// On successful uninstall, tell PMU to drive the download+install step
// for the new version. The impl layer has no LogosAPI access (it only
// communicates outward via the typed events), so we can't call
// package_downloader directly. Instead we emit upgradeUninstallDone
// with the pinned releaseTag — PMU subscribes to this event and reuses
// its existing download+install chain (downloadPackageAsync →
// installOnePackage). The user sees the row flip to "Installing" while
// the download runs, then to "Installed" (or "Failed") when it finishes.
bool ok = uninstallResult.value("success", false);
if (ok) {
LogosMap payload;
payload["name"] = packageName;
payload["releaseTag"] = releaseTag;
payload["mode"] = mode;
upgradeUninstallDone(payload.dump());
}
return uninstallResult;
}
LogosMap PackageManagerImpl::cancelUpgrade(const std::string& packageName,
const std::string& releaseTag)
{
PendingAction pa;
{
std::lock_guard<std::mutex> lock(m_stateMutex);
if (m_pendingAction.op != PendingOp::Upgrade
|| m_pendingAction.name != packageName
|| m_pendingAction.releaseTag != releaseTag) {
LogosMap response;
response["success"] = false;
response["error"] = "No matching pending upgrade for '" + packageName + "'";
return response;
}
// See cancelUninstall for why cancel also requires prior ack.
if (!m_pendingAction.acked) {
LogosMap response;
response["success"] = false;
response["error"] = "Pending upgrade for '" + packageName + "' has not been acknowledged";
return response;
}
pa = m_pendingAction;
m_pendingAction = {};
stopAckTimerLocked();
}
emitCancellation(pa, "user cancelled");
LogosMap response;
response["success"] = true;
return response;
}
LogosMap PackageManagerImpl::resetPendingAction()
{
std::lock_guard<std::mutex> lock(m_stateMutex);
m_pendingAction = {};
stopAckTimerLocked();
LogosMap response;
response["success"] = true;
return response;
}
// ---------------------------------------------------------------------------
// Multi-package gated uninstall
// ---------------------------------------------------------------------------
//
// Same protocol as requestUninstall — single pending slot, single ack, single
// confirm/cancel — extended to gate a batch of N packages. The destructive
// loop in confirmMultiUninstall calls doUninstall(name) per package, which
// emits per-package corePluginUninstalled / uiPluginUninstalled as today
namespace {
// Dedupe while preserving first-occurrence order. Used at the boundary of
// every multi-uninstall entry point so duplicate names in the caller's list
// can never cause a double-uninstall or a confirm/cancel mismatch.
static std::vector<std::string> dedupeNamesPreserveOrder(
const std::vector<std::string>& in)
{
std::vector<std::string> out;
out.reserve(in.size());
std::set<std::string> seen;
for (const auto& n : in) {
if (seen.insert(n).second) out.push_back(n);
}
return out;
}
} // namespace
LogosMap PackageManagerImpl::confirmInstall(const std::string& packageName)
{
// A fresh install removes nothing first — unlike confirmUpgrade there is no
// doUninstall step. Validate, capture the echo fields, and clear the gate in
// ONE critical section so a concurrent cancel / reset / ack-timeout can't swap
// the pending action out between the check and the capture (which would make
// installApproved carry an empty/wrong payload). Emit outside the lock —
// listeners may call back in synchronously.
LogosMap payload;
{
std::lock_guard<std::mutex> lock(m_stateMutex);
if (m_pendingAction.op != PendingOp::Install || m_pendingAction.name != packageName) {
LogosMap response;
response["success"] = false;
response["error"] = "No matching pending install for '" + packageName + "'";
return response;
}
if (!m_pendingAction.acked) {
LogosMap response;
response["success"] = false;
response["error"] = "Pending install for '" + packageName + "' has not been acknowledged";
return response;
}
payload["name"] = m_pendingAction.name;
payload["releaseTag"] = m_pendingAction.releaseTag;
payload["repositoryUrl"] = m_pendingAction.repositoryUrl;
m_pendingAction = {};
stopAckTimerLocked();
}
installApproved(payload.dump());
LogosMap response;
response["success"] = true;
return response;
}
LogosMap PackageManagerImpl::cancelInstall(const std::string& packageName)
{
PendingAction pa;
{
std::lock_guard<std::mutex> lock(m_stateMutex);
if (m_pendingAction.op != PendingOp::Install || m_pendingAction.name != packageName) {
LogosMap response;
response["success"] = false;
response["error"] = "No matching pending install for '" + packageName + "'";
return response;
}
// See cancelUninstall for why cancel also requires prior ack.
if (!m_pendingAction.acked) {
LogosMap response;
response["success"] = false;
response["error"] = "Pending install for '" + packageName + "' has not been acknowledged";
return response;
}
pa = m_pendingAction;
m_pendingAction = {};
stopAckTimerLocked();
}
emitCancellation(pa, "user cancelled");
LogosMap response;
response["success"] = true;
return response;
}
LogosMap PackageManagerImpl::requestMultiUninstall(const std::vector<std::string>& packageNamesIn)
{
LogosMap response;
if (packageNamesIn.empty()) {
response["success"] = false;
response["error"] = "Package list cannot be empty";
return response;
}
for (const auto& n : packageNamesIn) {
if (n.empty()) {
response["success"] = false;
response["error"] = "Package names cannot be empty";
return response;
}
}
// Dedupe immediately so every downstream check (embedded scan, dependents
// union, m_pendingAction.names storage, beforeMultiUninstall payload)
// operates on the canonical set.
const std::vector<std::string> packageNames =
dedupeNamesPreserveOrder(packageNamesIn);
std::unique_lock<std::mutex> lock(m_stateMutex);
if (m_pendingAction.op != PendingOp::None) {
response["success"] = false;
response["error"] = pendingDescriptionLocked();
return response;
}
std::vector<std::string> embedded;
for (const auto& n : packageNames) {
if (isEmbedded(n)) embedded.push_back(n);
}
if (!embedded.empty()) {
std::string msg = "Cannot uninstall embedded modules:";
for (const auto& n : embedded) msg += " '" + n + "'";
response["success"] = false;
response["error"] = msg;
return response;
}
m_pendingAction = {};
m_pendingAction.op = PendingOp::MultiUninstall;
m_pendingAction.names = packageNames;
m_pendingAction.acked = false;
// m_pendingAction.name intentionally left empty — ack matches against
// m_pendingAction.names directly; the cross-op blocking error message
// uses pendingDescriptionLocked() which handles the multi case.
std::set<std::string> batchSet(packageNames.begin(), packageNames.end());
std::vector<std::string> dedupedDeps;
std::set<std::string> seen;
for (const auto& n : packageNames) {
for (const auto& d : installedDependentsNames(n)) {
if (batchSet.count(d)) continue;
if (seen.insert(d).second) dedupedDeps.push_back(d);
}
}
LogosMap payload;
LogosList namesArr = LogosList::array();
for (const auto& n : packageNames) namesArr.push_back(n);
payload["names"] = namesArr;
LogosList depsArr = LogosList::array();
for (const auto& d : dedupedDeps) depsArr.push_back(d);
payload["installedDependents"] = depsArr;
startAckTimerLocked(lock);
lock.unlock();
beforeMultiUninstall(payload.dump());
response["success"] = true;
return response;
}
LogosMap PackageManagerImpl::confirmMultiUninstall(const std::vector<std::string>& packageNamesIn)
{
// Dedupe so callers can pass either the original or deduped form — the
// pending state always holds the deduped list (see requestMultiUninstall).
const std::vector<std::string> packageNames =
dedupeNamesPreserveOrder(packageNamesIn);
{
std::lock_guard<std::mutex> lock(m_stateMutex);
if (m_pendingAction.op != PendingOp::MultiUninstall || m_pendingAction.names != packageNames) {
LogosMap response;
response["success"] = false;
response["error"] = "No matching pending multi-uninstall";
return response;
}
if (!m_pendingAction.acked) {
LogosMap response;
response["success"] = false;
response["error"] = "Pending multi-uninstall has not been acknowledged";
return response;
}
m_pendingAction = {};
stopAckTimerLocked();
}
LogosList results = LogosList::array();
bool allOk = true;
for (const auto& n : packageNames) {
LogosMap one = doUninstall(n);
bool ok = one.value("success", false);
if (!ok) allOk = false;
LogosMap entry;
entry["name"] = n;
entry["success"] = ok;
if (one.contains("error")) entry["error"] = one["error"];
if (one.contains("removedFiles")) entry["removedFiles"] = one["removedFiles"];
results.push_back(entry);
}
LogosMap response;
response["success"] = allOk;
response["results"] = results;
return response;
}
LogosMap PackageManagerImpl::cancelMultiUninstall(const std::vector<std::string>& packageNamesIn)
{
// Dedupe so callers can pass either the original or deduped form — see
// confirmMultiUninstall for rationale.
const std::vector<std::string> packageNames =
dedupeNamesPreserveOrder(packageNamesIn);
PendingAction pa;
{
std::lock_guard<std::mutex> lock(m_stateMutex);
if (m_pendingAction.op != PendingOp::MultiUninstall || m_pendingAction.names != packageNames) {
LogosMap response;
response["success"] = false;
response["error"] = "No matching pending multi-uninstall";
return response;
}
if (!m_pendingAction.acked) {
LogosMap response;
response["success"] = false;
response["error"] = "Pending multi-uninstall has not been acknowledged";
return response;
}
pa = m_pendingAction;
m_pendingAction = {};
stopAckTimerLocked();
}
emitCancellation(pa, "user cancelled");
LogosMap response;
response["success"] = true;
return response;
}