2026-03-18 10:24:51 -04:00
|
|
|
#include "package_manager_impl.h"
|
2026-03-27 09:02:54 -03:00
|
|
|
#include <package_manager_lib.h>
|
2026-04-08 18:39:29 -03:00
|
|
|
#include <lgx.h>
|
2026-04-10 11:33:44 -04:00
|
|
|
#include <algorithm>
|
2026-04-16 19:34:03 -03:00
|
|
|
#include <chrono>
|
2026-04-10 11:33:44 -04:00
|
|
|
#include <filesystem>
|
|
|
|
|
#include <iostream>
|
2026-04-16 19:34:03 -03:00
|
|
|
#include <limits>
|
2026-05-05 22:54:23 +05:30
|
|
|
#include <set>
|
2026-04-16 19:34:03 -03:00
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 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;
|
2026-06-22 13:43:41 +02:00
|
|
|
m["displayName"] = p.displayName;
|
2026-04-16 19:34:03 -03:00
|
|
|
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;
|
2026-08-11 20:53:54 +02:00
|
|
|
m["manifestVersion"] = p.manifestVersion;
|
2026-04-16 19:34:03 -03:00
|
|
|
m["view"] = p.view;
|
|
|
|
|
|
|
|
|
|
LogosList deps = LogosList::array();
|
|
|
|
|
for (const auto& d : p.dependencies) deps.push_back(d);
|
|
|
|
|
m["dependencies"] = deps;
|
|
|
|
|
|
2026-08-27 12:00:39 -03:00
|
|
|
// 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;
|
|
|
|
|
|
2026-04-16 19:34:03 -03:00
|
|
|
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));
|
2026-08-27 12:00:39 -03:00
|
|
|
// 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)) {
|
2026-04-16 19:34:03 -03:00
|
|
|
m["version"] = n.version;
|
|
|
|
|
m["installType"] = std::string(installTypeToString(n.installType));
|
|
|
|
|
} else {
|
|
|
|
|
m["version"] = "";
|
|
|
|
|
m["installType"] = "";
|
|
|
|
|
}
|
2026-08-27 12:00:39 -03:00
|
|
|
// 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;
|
2026-04-16 19:34:03 -03:00
|
|
|
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
|
2026-03-18 10:24:51 -04:00
|
|
|
|
|
|
|
|
PackageManagerImpl::PackageManagerImpl()
|
|
|
|
|
: m_lib(nullptr)
|
|
|
|
|
{
|
2026-03-27 09:02:54 -03:00
|
|
|
m_lib = new PackageManagerLib();
|
2026-03-18 10:24:51 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PackageManagerImpl::~PackageManagerImpl()
|
|
|
|
|
{
|
2026-06-02 18:43:59 -03:00
|
|
|
// 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.
|
2026-04-16 19:34:03 -03:00
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lk(m_stateMutex);
|
|
|
|
|
m_ackShutdown = true;
|
|
|
|
|
++m_ackGeneration;
|
|
|
|
|
}
|
|
|
|
|
m_ackCv.notify_all();
|
|
|
|
|
if (m_ackThread.joinable()) m_ackThread.join();
|
|
|
|
|
|
2026-03-18 10:24:51 -04:00
|
|
|
delete m_lib;
|
|
|
|
|
m_lib = nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
LogosMap PackageManagerImpl::installPlugin(const std::string& pluginPath, bool skipIfNotNewerVersion)
|
2026-03-18 10:24:51 -04:00
|
|
|
{
|
2026-03-27 09:02:54 -03:00
|
|
|
std::string errorMsg;
|
|
|
|
|
std::string installedPluginPath;
|
|
|
|
|
bool isCoreModule = false;
|
|
|
|
|
std::string result = m_lib->installPluginFile(
|
2026-04-10 11:33:44 -04:00
|
|
|
pluginPath, errorMsg, skipIfNotNewerVersion,
|
2026-03-27 09:02:54 -03:00
|
|
|
&installedPluginPath, &isCoreModule
|
|
|
|
|
);
|
|
|
|
|
|
2026-08-11 20:31:31 -03:00
|
|
|
// 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.
|
2026-03-27 09:02:54 -03:00
|
|
|
bool success = !result.empty();
|
2026-08-11 20:31:31 -03:00
|
|
|
const std::string reportedPath = installedPluginPath.empty() ? result : installedPluginPath;
|
2026-03-27 09:02:54 -03:00
|
|
|
|
2026-08-11 20:31:31 -03:00
|
|
|
if (success) {
|
2026-04-10 11:33:44 -04:00
|
|
|
if (isCoreModule) {
|
2026-08-11 20:31:31 -03:00
|
|
|
corePluginFileInstalled(reportedPath);
|
2026-04-10 11:33:44 -04:00
|
|
|
} else {
|
2026-08-11 20:31:31 -03:00
|
|
|
uiPluginFileInstalled(reportedPath);
|
2026-04-10 11:33:44 -04:00
|
|
|
}
|
2026-03-27 09:02:54 -03:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 18:39:29 -03:00
|
|
|
// Get signature info for the response
|
2026-04-10 11:33:44 -04:00
|
|
|
auto sigResult = m_lib->verifyPackageSignature(pluginPath);
|
2026-04-08 18:39:29 -03:00
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
std::string stem = std::filesystem::path(pluginPath).stem().string();
|
|
|
|
|
|
|
|
|
|
LogosMap response;
|
|
|
|
|
response["name"] = stem;
|
2026-08-11 20:31:31 -03:00
|
|
|
response["path"] = success ? reportedPath : std::string();
|
2026-03-27 09:02:54 -03:00
|
|
|
response["isCoreModule"] = isCoreModule;
|
|
|
|
|
if (!success) {
|
2026-04-10 11:33:44 -04:00
|
|
|
response["error"] = errorMsg;
|
2026-03-27 09:02:54 -03:00
|
|
|
}
|
2026-04-08 18:39:29 -03:00
|
|
|
|
|
|
|
|
// Add signature info
|
|
|
|
|
if (sigResult.is_signed) {
|
2026-04-10 11:33:44 -04:00
|
|
|
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;
|
2026-04-08 18:39:29 -03:00
|
|
|
} else if (!sigResult.error.empty()) {
|
2026-04-10 11:33:44 -04:00
|
|
|
response["signatureStatus"] = std::string("error");
|
|
|
|
|
response["signatureError"] = sigResult.error;
|
2026-04-08 18:39:29 -03:00
|
|
|
} else {
|
2026-04-10 11:33:44 -04:00
|
|
|
response["signatureStatus"] = std::string("unsigned");
|
2026-04-08 18:39:29 -03:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 09:02:54 -03:00
|
|
|
return response;
|
2026-03-18 10:24:51 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-16 19:34:03 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
LogosList PackageManagerImpl::getInstalledPackages()
|
2026-03-18 10:24:51 -04:00
|
|
|
{
|
2026-04-16 19:34:03 -03:00
|
|
|
return toLogosList(m_lib->getInstalledPackages());
|
2026-03-18 10:24:51 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
LogosList PackageManagerImpl::getInstalledModules()
|
2026-03-18 10:24:51 -04:00
|
|
|
{
|
2026-04-16 19:34:03 -03:00
|
|
|
return toLogosList(m_lib->getInstalledModules());
|
2026-03-18 10:24:51 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
LogosList PackageManagerImpl::getInstalledUiPlugins()
|
2026-03-18 10:24:51 -04:00
|
|
|
{
|
2026-04-16 19:34:03 -03:00
|
|
|
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;
|
|
|
|
|
|
2026-06-02 18:43:59 -03:00
|
|
|
if (moduleType == "core") {
|
|
|
|
|
corePluginUninstalled(packageName);
|
|
|
|
|
} else {
|
|
|
|
|
uiPluginUninstalled(packageName);
|
2026-04-16 19:34:03 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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);
|
2026-03-18 10:24:51 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
std::vector<std::string> PackageManagerImpl::getValidVariants()
|
2026-03-18 10:24:51 -04:00
|
|
|
{
|
2026-04-10 11:33:44 -04:00
|
|
|
return PackageManagerLib::platformVariantsToTry();
|
2026-03-18 10:24:51 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
void PackageManagerImpl::setEmbeddedModulesDirectory(const std::string& dir)
|
2026-03-18 10:24:51 -04:00
|
|
|
{
|
2026-04-10 11:33:44 -04:00
|
|
|
m_lib->setEmbeddedModulesDirectory(dir);
|
2026-03-18 10:24:51 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
void PackageManagerImpl::addEmbeddedModulesDirectory(const std::string& dir)
|
2026-03-18 10:24:51 -04:00
|
|
|
{
|
2026-04-10 11:33:44 -04:00
|
|
|
m_lib->addEmbeddedModulesDirectory(dir);
|
2026-03-18 10:24:51 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
void PackageManagerImpl::setEmbeddedUiPluginsDirectory(const std::string& dir)
|
2026-03-18 10:24:51 -04:00
|
|
|
{
|
2026-04-10 11:33:44 -04:00
|
|
|
m_lib->setEmbeddedUiPluginsDirectory(dir);
|
2026-03-18 10:24:51 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
void PackageManagerImpl::addEmbeddedUiPluginsDirectory(const std::string& dir)
|
2026-03-18 10:24:51 -04:00
|
|
|
{
|
2026-04-10 11:33:44 -04:00
|
|
|
m_lib->addEmbeddedUiPluginsDirectory(dir);
|
2026-03-18 10:24:51 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
void PackageManagerImpl::setUserModulesDirectory(const std::string& dir)
|
2026-03-18 10:24:51 -04:00
|
|
|
{
|
2026-04-10 11:33:44 -04:00
|
|
|
m_lib->setUserModulesDirectory(dir);
|
2026-03-18 10:24:51 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
void PackageManagerImpl::setUserUiPluginsDirectory(const std::string& dir)
|
2026-03-27 15:55:47 -03:00
|
|
|
{
|
2026-04-10 11:33:44 -04:00
|
|
|
m_lib->setUserUiPluginsDirectory(dir);
|
2026-03-27 15:55:47 -03:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
void PackageManagerImpl::setSignaturePolicy(const std::string& policy)
|
2026-03-27 15:55:47 -03:00
|
|
|
{
|
2026-04-10 11:33:44 -04:00
|
|
|
std::string p = policy;
|
|
|
|
|
std::transform(p.begin(), p.end(), p.begin(), ::tolower);
|
2026-04-08 18:39:29 -03:00
|
|
|
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 {
|
2026-04-10 11:33:44 -04:00
|
|
|
std::cerr << "PackageManagerImpl::setSignaturePolicy: invalid policy '"
|
|
|
|
|
<< policy << "' - expected one of: none, warn, require\n";
|
2026-04-08 18:39:29 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
void PackageManagerImpl::setKeyringDirectory(const std::string& dir)
|
2026-04-08 18:39:29 -03:00
|
|
|
{
|
2026-04-10 11:33:44 -04:00
|
|
|
m_lib->setKeyringDirectory(dir);
|
2026-04-08 18:39:29 -03:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
LogosMap PackageManagerImpl::verifyPackage(const std::string& lgxPath)
|
2026-04-08 18:39:29 -03:00
|
|
|
{
|
2026-04-10 11:33:44 -04:00
|
|
|
auto result = m_lib->verifyPackageSignature(lgxPath);
|
2026-04-08 18:39:29 -03:00
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
LogosMap response;
|
2026-04-08 18:39:29 -03:00
|
|
|
response["isSigned"] = result.is_signed;
|
|
|
|
|
response["signatureValid"] = result.signature_valid;
|
|
|
|
|
response["packageValid"] = result.package_valid;
|
2026-04-10 11:33:44 -04:00
|
|
|
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;
|
2026-04-08 18:39:29 -03:00
|
|
|
return response;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
LogosMap PackageManagerImpl::addTrustedKey(const std::string& name, const std::string& did,
|
|
|
|
|
const std::string& displayName, const std::string& url)
|
2026-04-08 18:39:29 -03:00
|
|
|
{
|
|
|
|
|
std::string keyringDir = m_lib->keyringDirectory();
|
|
|
|
|
const char* keyringDirPtr = keyringDir.empty() ? nullptr : keyringDir.c_str();
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
lgx_result_t res = lgx_keyring_add(
|
2026-04-08 18:39:29 -03:00
|
|
|
keyringDirPtr,
|
2026-04-10 11:33:44 -04:00
|
|
|
name.c_str(),
|
|
|
|
|
did.c_str(),
|
|
|
|
|
displayName.empty() ? nullptr : displayName.c_str(),
|
|
|
|
|
url.empty() ? nullptr : url.c_str()
|
2026-04-08 18:39:29 -03:00
|
|
|
);
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
LogosMap response;
|
|
|
|
|
response["success"] = static_cast<bool>(res.success);
|
|
|
|
|
if (!res.success && res.error)
|
|
|
|
|
response["error"] = std::string(res.error);
|
2026-04-08 18:39:29 -03:00
|
|
|
return response;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
LogosMap PackageManagerImpl::removeTrustedKey(const std::string& name)
|
2026-04-08 18:39:29 -03:00
|
|
|
{
|
|
|
|
|
std::string keyringDir = m_lib->keyringDirectory();
|
|
|
|
|
const char* keyringDirPtr = keyringDir.empty() ? nullptr : keyringDir.c_str();
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
lgx_result_t res = lgx_keyring_remove(
|
2026-04-08 18:39:29 -03:00
|
|
|
keyringDirPtr,
|
2026-04-10 11:33:44 -04:00
|
|
|
name.c_str()
|
2026-04-08 18:39:29 -03:00
|
|
|
);
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
LogosMap response;
|
|
|
|
|
response["success"] = static_cast<bool>(res.success);
|
|
|
|
|
if (!res.success && res.error)
|
|
|
|
|
response["error"] = std::string(res.error);
|
2026-04-08 18:39:29 -03:00
|
|
|
return response;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
LogosList PackageManagerImpl::listTrustedKeys()
|
2026-04-08 18:39:29 -03:00
|
|
|
{
|
|
|
|
|
std::string keyringDir = m_lib->keyringDirectory();
|
|
|
|
|
const char* keyringDirPtr = keyringDir.empty() ? nullptr : keyringDir.c_str();
|
|
|
|
|
|
|
|
|
|
lgx_keyring_list_t list = lgx_keyring_list(keyringDirPtr);
|
|
|
|
|
|
2026-04-10 11:33:44 -04:00
|
|
|
LogosList result = LogosList::array();
|
2026-04-08 18:39:29 -03:00
|
|
|
for (size_t i = 0; i < list.count; ++i) {
|
2026-04-10 11:33:44 -04:00
|
|
|
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);
|
2026-04-08 18:39:29 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lgx_free_keyring_list(list);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
2026-04-16 19:34:03 -03:00
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Gated uninstall / upgrade flow
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
const char* PackageManagerImpl::opName(PendingOp op)
|
|
|
|
|
{
|
|
|
|
|
switch (op) {
|
2026-05-05 22:54:23 +05:30
|
|
|
case PendingOp::Uninstall: return "uninstall";
|
|
|
|
|
case PendingOp::Upgrade: return "upgrade";
|
2026-07-16 23:12:23 -03:00
|
|
|
case PendingOp::Install: return "install";
|
2026-05-05 22:54:23 +05:30
|
|
|
case PendingOp::MultiUninstall: return "multi-uninstall";
|
|
|
|
|
case PendingOp::None: return "none";
|
2026-04-16 19:34:03 -03:00
|
|
|
}
|
|
|
|
|
return "none";
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 22:54:23 +05:30
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 19:34:03 -03:00
|
|
|
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";
|
|
|
|
|
|
2026-06-02 18:43:59 -03:00
|
|
|
// Release the lock before emitting — event emission marshals through a
|
2026-04-16 19:34:03 -03:00
|
|
|
// 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) {
|
2026-05-05 22:54:23 +05:30
|
|
|
payload["name"] = pa.name;
|
2026-04-16 19:34:03 -03:00
|
|
|
payload["releaseTag"] = pa.releaseTag;
|
2026-06-02 18:43:59 -03:00
|
|
|
upgradeCancelled(payload.dump());
|
2026-04-16 19:34:03 -03:00
|
|
|
} else if (pa.op == PendingOp::Uninstall) {
|
2026-05-05 22:54:23 +05:30
|
|
|
payload["name"] = pa.name;
|
2026-06-02 18:43:59 -03:00
|
|
|
uninstallCancelled(payload.dump());
|
2026-07-16 23:12:23 -03:00
|
|
|
} else if (pa.op == PendingOp::Install) {
|
|
|
|
|
payload["name"] = pa.name;
|
|
|
|
|
payload["releaseTag"] = pa.releaseTag;
|
|
|
|
|
payload["repositoryUrl"] = pa.repositoryUrl;
|
|
|
|
|
installCancelled(payload.dump());
|
2026-05-05 22:54:23 +05:30
|
|
|
} else if (pa.op == PendingOp::MultiUninstall) {
|
|
|
|
|
LogosList names = LogosList::array();
|
|
|
|
|
for (const auto& n : pa.names) names.push_back(n);
|
|
|
|
|
payload["names"] = names;
|
2026-06-02 18:43:59 -03:00
|
|
|
multiUninstallCancelled(payload.dump());
|
2026-04-16 19:34:03 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-05-05 22:54:23 +05:30
|
|
|
response["error"] = pendingDescriptionLocked();
|
2026-04-16 19:34:03 -03:00
|
|
|
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
|
2026-06-02 18:43:59 -03:00
|
|
|
// don't race against a concurrent slot). The event emission itself is
|
|
|
|
|
// deferred until after the unlock — see the reentrancy note in ackTimerWorker.
|
2026-04-16 19:34:03 -03:00
|
|
|
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();
|
2026-06-02 18:43:59 -03:00
|
|
|
beforeUninstall(payload.dump());
|
2026-04-16 19:34:03 -03:00
|
|
|
|
|
|
|
|
response["success"] = true;
|
|
|
|
|
return response;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 23:12:23 -03:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 19:34:03 -03:00
|
|
|
LogosMap PackageManagerImpl::requestUpgrade(const std::string& packageName,
|
|
|
|
|
const std::string& releaseTag,
|
2026-07-16 23:12:23 -03:00
|
|
|
int64_t mode,
|
|
|
|
|
const std::string& depChanges)
|
2026-04-16 19:34:03 -03:00
|
|
|
{
|
|
|
|
|
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;
|
2026-05-05 22:54:23 +05:30
|
|
|
response["error"] = pendingDescriptionLocked();
|
2026-04-16 19:34:03 -03:00
|
|
|
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;
|
2026-07-16 23:12:23 -03:00
|
|
|
attachDepChanges(payload, depChanges);
|
2026-04-16 19:34:03 -03:00
|
|
|
|
|
|
|
|
startAckTimerLocked(lock);
|
|
|
|
|
|
|
|
|
|
lock.unlock();
|
2026-06-02 18:43:59 -03:00
|
|
|
beforeUpgrade(payload.dump());
|
2026-04-16 19:34:03 -03:00
|
|
|
|
|
|
|
|
response["success"] = true;
|
|
|
|
|
return response;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 23:12:23 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 19:34:03 -03:00
|
|
|
LogosMap PackageManagerImpl::ackPendingAction(const std::string& packageName)
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(m_stateMutex);
|
|
|
|
|
LogosMap response;
|
2026-05-05 22:54:23 +05:30
|
|
|
|
|
|
|
|
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) {
|
2026-04-16 19:34:03 -03:00
|
|
|
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
|
2026-06-02 18:43:59 -03:00
|
|
|
// communicates outward via the typed events), so we can't call
|
2026-04-16 19:34:03 -03:00
|
|
|
// 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);
|
2026-06-02 18:43:59 -03:00
|
|
|
if (ok) {
|
2026-04-16 19:34:03 -03:00
|
|
|
LogosMap payload;
|
|
|
|
|
payload["name"] = packageName;
|
|
|
|
|
payload["releaseTag"] = releaseTag;
|
|
|
|
|
payload["mode"] = mode;
|
2026-06-02 18:43:59 -03:00
|
|
|
upgradeUninstallDone(payload.dump());
|
2026-04-16 19:34:03 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2026-05-05 22:54:23 +05:30
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 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
|
|
|
|
|
|
2026-07-16 23:12:23 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 22:54:23 +05:30
|
|
|
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();
|
2026-06-02 18:43:59 -03:00
|
|
|
beforeMultiUninstall(payload.dump());
|
2026-05-05 22:54:23 +05:30
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|