ExtensionSystem: Allow updating and removing plugins

Change-Id: Ie81e697c97fbfe0dc66346f42d467e1a697606c3
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
This commit is contained in:
Marcus Tillmanns
2025-01-30 08:46:05 +01:00
parent e32613cfe2
commit cfda5829af
11 changed files with 391 additions and 49 deletions

View File

@@ -839,11 +839,17 @@ int main(int argc, char **argv)
// Make sure we honor the system's proxy settings // Make sure we honor the system's proxy settings
QNetworkProxyFactory::setUseSystemConfiguration(true); QNetworkProxyFactory::setUseSystemConfiguration(true);
// We need to install plugins before we scan for them.
PluginManager::installPluginsAfterRestart();
// Load // Load
const QStringList pluginPaths = installPluginPaths + options.customPluginPaths; const QStringList pluginPaths = installPluginPaths + options.customPluginPaths;
PluginManager::setPluginPaths( PluginManager::setPluginPaths(
getPluginPaths() + Utils::transform(pluginPaths, &FilePath::fromUserInput)); getPluginPaths() + Utils::transform(pluginPaths, &FilePath::fromUserInput));
// We need to remove plugins once we have scanned for them.
PluginManager::removePluginsAfterRestart();
QMap<QString, QString> foundAppOptions; QMap<QString, QString> foundAppOptions;
if (pluginArguments.size() > 1) { if (pluginArguments.size() > 1) {
QMap<QString, bool> appOptions; QMap<QString, bool> appOptions;

View File

@@ -347,6 +347,27 @@ void PluginManager::addPlugins(const PluginSpecs &specs)
d->addPlugins(specs); d->addPlugins(specs);
} }
void PluginManager::removePluginsAfterRestart()
{
d->removePluginsAfterRestart();
}
void PluginManager::installPluginsAfterRestart()
{
d->installPluginsAfterRestart();
}
void PluginManager::removePluginOnRestart(const QString &id)
{
d->removePluginOnNextRestart(id);
}
void PluginManager::installPluginOnRestart(
const Utils::FilePath &source, const Utils::FilePath &destination)
{
d->installPluginOnNextRestart(source, destination);
}
/*! /*!
Returns \c true if any plugin has errors even though it is enabled. Returns \c true if any plugin has errors even though it is enabled.
Most useful to call after loadPlugins(). Most useful to call after loadPlugins().
@@ -1689,6 +1710,11 @@ PluginSpec *PluginManager::specForPlugin(IPlugin *plugin)
return findOrDefault(d->pluginSpecs, equal(&PluginSpec::plugin, plugin)); return findOrDefault(d->pluginSpecs, equal(&PluginSpec::plugin, plugin));
} }
bool PluginManager::takePluginIdForRemoval(const QString &id)
{
return d->m_pluginsToRemove.remove(id);
}
static QString pluginListString(const QSet<PluginSpec *> &plugins) static QString pluginListString(const QSet<PluginSpec *> &plugins)
{ {
QStringList names = Utils::transform<QList>(plugins, &PluginSpec::name); QStringList names = Utils::transform<QList>(plugins, &PluginSpec::name);
@@ -1899,6 +1925,36 @@ static const FilePaths pluginFiles(const FilePaths &pluginPaths)
return pluginFiles; return pluginFiles;
} }
bool PluginManagerPrivate::removePlugin(const QString &pluginId)
{
PluginSpec *existingSpec
= Utils::findOrDefault(pluginSpecs, Utils::equal(&PluginSpec::id, pluginId));
if (existingSpec) {
QTC_ASSERT(existingSpec->state() == PluginSpec::State::Resolved, return false);
const Result removeResult = existingSpec->removePluginFiles();
if (!removeResult) {
qCWarning(pluginLog) << "Failed to remove plugin files for" << pluginId << ":"
<< removeResult.error();
return false;
}
for (QList<PluginSpec *> &category : pluginCategories) {
category.removeOne(existingSpec);
}
if (pluginSpecs.removeOne(existingSpec)) {
delete existingSpec;
return true;
}
}
// This is used by other plugin managers like Lua that is not loaded yet.
m_pluginsToRemove << pluginId;
return true;
}
void PluginManagerPrivate::addPlugins(const PluginSpecs &specs) void PluginManagerPrivate::addPlugins(const PluginSpecs &specs)
{ {
pluginSpecs += specs; pluginSpecs += specs;
@@ -1928,6 +1984,108 @@ void PluginManagerPrivate::addPlugins(const PluginSpecs &specs)
emit q->pluginsChanged(); emit q->pluginsChanged();
} }
static const char PLUGINS_TO_INSTALL_KEY[] = "PluginsToInstall";
static const char PLUGINS_TO_REMOVE_KEY[] = "PluginsToRemove";
void PluginManagerPrivate::removePluginOnNextRestart(const QString &pluginId)
{
settings->setValue(
PLUGINS_TO_REMOVE_KEY, settings->value(PLUGINS_TO_REMOVE_KEY).toStringList() << pluginId);
settings->sync();
}
static QList<QPair<FilePath, FilePath>> readPluginInstallList(QtcSettings *settings)
{
int size = settings->beginReadArray(PLUGINS_TO_INSTALL_KEY);
QList<QPair<FilePath, FilePath>> installList;
for (int i = 0; i < size; ++i) {
settings->setArrayIndex(i);
installList.append(
{FilePath::fromVariant(settings->value("src")),
FilePath::fromVariant(settings->value("dest"))});
}
settings->endArray();
return installList;
}
void PluginManagerPrivate::installPluginOnNextRestart(
const Utils::FilePath &src, const Utils::FilePath &dest)
{
const QList<QPair<FilePath, FilePath>> list = readPluginInstallList(settings)
<< qMakePair(src, dest);
settings->beginWriteArray(PLUGINS_TO_INSTALL_KEY);
for (int i = 0; i < list.size(); ++i) {
settings->setArrayIndex(i);
settings->setValue("src", list.at(i).first.toVariant());
settings->setValue("dest", list.at(i).second.toVariant());
}
settings->endArray();
settings->sync();
}
void PluginManagerPrivate::removePluginsAfterRestart()
{
const QStringList removeList = settings->value(PLUGINS_TO_REMOVE_KEY).toStringList();
for (const QString &pluginId : removeList)
removePlugin(pluginId);
settings->remove(PLUGINS_TO_REMOVE_KEY);
}
void PluginManagerPrivate::installPluginsAfterRestart()
{
QTC_CHECK(pluginSpecs.isEmpty());
QList<QPair<FilePath, FilePath>> installList = readPluginInstallList(settings);
const Utils::FilePaths pluginPaths = PluginManager::pluginPaths();
for (const auto &[src, dest] : installList) {
if (!src.exists()) {
qCWarning(pluginLog()) << "Cannot install source " << src << ", it does not exist";
continue;
}
if (dest.isDir()) {
if (auto result = dest.removeRecursively(); !result) {
qCWarning(pluginLog()) << "Failed to remove" << dest << ":" << result.error();
continue;
}
} else if (dest.isFile()) {
if (const Result result = dest.removeFile(); !result) {
qCWarning(pluginLog()) << "Failed to remove" << dest << ":" << result.error();
continue;
}
}
if (src.isFile()) {
if (!dest.createDir()) {
qCWarning(pluginLog()) << "Cannot install file" << src << "to" << dest
<< "because the destination directory cannot be created";
continue;
}
}
Utils::Result result = src.isDir() ? src.copyRecursively(dest)
: src.copyFile(dest / src.fileName());
if (!result) {
qCWarning(pluginLog())
<< "Failed to install" << src << "to" << dest << ":" << result.error();
continue;
}
result = src.isDir() ? src.removeRecursively() : src.removeFile();
if (!result)
qCWarning(pluginLog())
<< "Failed to remove the source file in" << src << ":" << result.error();
}
settings->remove(PLUGINS_TO_INSTALL_KEY);
}
/*! /*!
\internal \internal
*/ */
@@ -1954,6 +2112,13 @@ void PluginManagerPrivate::readPluginPaths()
newSpecs.append(spec->release()); newSpecs.append(spec->release());
} }
newSpecs = Utils::filtered(newSpecs, [this](PluginSpec *spec) {
return pluginById(spec->id()) == nullptr;
});
if (newSpecs.empty())
return;
addPlugins(newSpecs); addPlugins(newSpecs);
} }

View File

@@ -80,9 +80,19 @@ public:
static const QSet<PluginSpec *> pluginsRequiredByPlugin(PluginSpec *spec); static const QSet<PluginSpec *> pluginsRequiredByPlugin(PluginSpec *spec);
static void checkForProblematicPlugins(); static void checkForProblematicPlugins();
static PluginSpec *specForPlugin(IPlugin *plugin); static PluginSpec *specForPlugin(IPlugin *plugin);
static bool takePluginIdForRemoval(const QString &id);
static void addPlugins(const QVector<PluginSpec *> &specs); static void addPlugins(const QVector<PluginSpec *> &specs);
static void reInstallPlugins();
static void removePluginOnRestart(const QString &id);
static void installPluginOnRestart(
const Utils::FilePath &source, const Utils::FilePath &destination);
static void removePluginsAfterRestart();
static void installPluginsAfterRestart();
// UI // UI
static std::optional<QSet<PluginSpec *>> askForEnablingPlugins( static std::optional<QSet<PluginSpec *>> askForEnablingPlugins(
QWidget *dialogParent, const QSet<PluginSpec *> &plugins, bool enable); QWidget *dialogParent, const QSet<PluginSpec *> &plugins, bool enable);

View File

@@ -52,6 +52,8 @@ public:
void loadPluginsAtRuntime(const QSet<PluginSpec *> &plugins); void loadPluginsAtRuntime(const QSet<PluginSpec *> &plugins);
void addPlugins(const QVector<PluginSpec *> &specs); void addPlugins(const QVector<PluginSpec *> &specs);
bool removePlugin(const QString &pluginId);
void shutdown(); void shutdown();
void setPluginPaths(const Utils::FilePaths &paths); void setPluginPaths(const Utils::FilePaths &paths);
const QVector<ExtensionSystem::PluginSpec *> loadQueue(); const QVector<ExtensionSystem::PluginSpec *> loadQueue();
@@ -70,6 +72,13 @@ public:
bool acceptTermsAndConditions(PluginSpec *spec); bool acceptTermsAndConditions(PluginSpec *spec);
void setAcceptTermsAndConditionsCallback(const std::function<bool(PluginSpec *)> &callback); void setAcceptTermsAndConditionsCallback(const std::function<bool(PluginSpec *)> &callback);
void readPluginPaths();
void removePluginsAfterRestart();
void installPluginsAfterRestart();
void removePluginOnNextRestart(const QString &pluginId);
void installPluginOnNextRestart(const Utils::FilePath &src, const Utils::FilePath &dest);
class TestSpec { class TestSpec {
public: public:
@@ -143,12 +152,13 @@ public:
PluginManager::ProcessData m_creatorProcessData; PluginManager::ProcessData m_creatorProcessData;
QSet<QString> m_pluginsToRemove;
private: private:
PluginManager *q; PluginManager *q;
void startDelayedInitialize(); void startDelayedInitialize();
void readPluginPaths();
bool loadQueue(PluginSpec *spec, bool loadQueue(PluginSpec *spec,
QVector<ExtensionSystem::PluginSpec *> &queue, QVector<ExtensionSystem::PluginSpec *> &queue,
QVector<ExtensionSystem::PluginSpec *> &circularityCheckQueue); QVector<ExtensionSystem::PluginSpec *> &circularityCheckQueue);

View File

@@ -1453,4 +1453,33 @@ QList<PluginSpec *> pluginSpecsFromArchive(const Utils::FilePath &path)
return results; return results;
} }
Utils::Result PluginSpec::removePluginFiles() const
{
if (isSystemPlugin())
return Result::Error(Tr::tr("Cannot remove system plugins."));
// Try to figure out where we are ...
const Utils::FilePaths pluginPaths = PluginManager::pluginPaths();
for (const FilePath &pluginPath : pluginPaths) {
if (location().isChildOf(pluginPath)) {
const FilePath rootFolder = location().relativeChildPath(pluginPath);
if (rootFolder.isEmpty())
return Result::Error(Tr::tr("Could not determine root folder."));
const FilePath pathToDelete = pluginPath
/ rootFolder.pathComponents().first().toString();
return pathToDelete.removeRecursively();
}
}
return filePath().removeFile();
}
bool PluginSpec::isSystemPlugin() const
{
return !filePath().isChildOf(appInfo().userPluginsRoot)
&& !filePath().isChildOf(appInfo().userLuaPlugins);
}
} // namespace ExtensionSystem } // namespace ExtensionSystem

View File

@@ -161,6 +161,9 @@ public:
virtual Utils::FilePath installLocation(bool inUserFolder) const = 0; virtual Utils::FilePath installLocation(bool inUserFolder) const = 0;
virtual Utils::Result removePluginFiles() const;
virtual bool isSystemPlugin() const;
protected: protected:
virtual void setEnabledByDefault(bool value); virtual void setEnabledByDefault(bool value);
virtual void setEnabledIndirectly(bool value); virtual void setEnabledIndirectly(bool value);

View File

@@ -51,6 +51,7 @@ struct Data
bool installIntoApplication = false; bool installIntoApplication = false;
std::unique_ptr<PluginSpec> pluginSpec = nullptr; std::unique_ptr<PluginSpec> pluginSpec = nullptr;
bool loadImmediately = false; bool loadImmediately = false;
bool prepareForUpdate = false;
}; };
static bool hasLibSuffix(const FilePath &path) static bool hasLibSuffix(const FilePath &path)
@@ -127,13 +128,20 @@ public:
using CheckResult = expected_str<PluginSpec *>; using CheckResult = expected_str<PluginSpec *>;
static Result checkPlugin(PluginSpec *spec) static Result checkPlugin(PluginSpec *spec, bool update)
{ {
if (Utils::anyOf(PluginManager::plugins(), [spec](PluginSpec *other) { const bool pluginAlreadyExists
= Utils::anyOf(PluginManager::plugins(), [spec](PluginSpec *other) {
return other->id() == spec->id(); return other->id() == spec->id();
})) });
if (!update && pluginAlreadyExists) {
return Result::Error( return Result::Error(
Tr::tr("A plugin with ID \"%1\" is already installed.").arg(spec->id())); Tr::tr("A plugin with ID \"%1\" is already installed.").arg(spec->id()));
} else if (update && !pluginAlreadyExists) {
return Result::Error(Tr::tr("No plugin with ID \"%1\" is installed.").arg(spec->id()));
}
if (!spec->resolveDependencies(PluginManager::plugins())) { if (!spec->resolveDependencies(PluginManager::plugins())) {
return Result::Error( return Result::Error(
Tr::tr("Plugin failed to resolve dependencies:") + " " + spec->errorString()); Tr::tr("Plugin failed to resolve dependencies:") + " " + spec->errorString());
@@ -142,18 +150,18 @@ static Result checkPlugin(PluginSpec *spec)
} }
static expected_str<std::unique_ptr<PluginSpec>> checkPlugin( static expected_str<std::unique_ptr<PluginSpec>> checkPlugin(
expected_str<std::unique_ptr<PluginSpec>> spec) expected_str<std::unique_ptr<PluginSpec>> spec, bool update)
{ {
if (!spec) if (!spec)
return spec; return spec;
const Result ok = checkPlugin(spec->get()); const Result ok = checkPlugin(spec->get(), update);
if (ok) if (ok)
return spec; return spec;
return Utils::make_unexpected(ok.error()); return Utils::make_unexpected(ok.error());
} }
// Async. Result is set if any issue was found. // Async. Result is set if any issue was found.
void checkContents(QPromise<CheckResult> &promise, const FilePath &tempDir) void checkContents(QPromise<CheckResult> &promise, const FilePath &tempDir, bool update)
{ {
QList<PluginSpec *> plugins = pluginSpecsFromArchive(tempDir); QList<PluginSpec *> plugins = pluginSpecsFromArchive(tempDir);
if (plugins.isEmpty()) { if (plugins.isEmpty()) {
@@ -167,7 +175,7 @@ void checkContents(QPromise<CheckResult> &promise, const FilePath &tempDir)
} }
PluginSpec *plugin = plugins.front(); PluginSpec *plugin = plugins.front();
const Result ok = checkPlugin(plugin); const Result ok = checkPlugin(plugin, update);
if (!ok) { if (!ok) {
promise.addResult(Utils::make_unexpected(ok.error())); promise.addResult(Utils::make_unexpected(ok.error()));
delete plugin; delete plugin;
@@ -214,8 +222,8 @@ public:
emit completeChanged(); emit completeChanged();
if (hasLibSuffix(m_data->sourcePath)) { if (hasLibSuffix(m_data->sourcePath)) {
m_cancelButton->setVisible(false); m_cancelButton->setVisible(false);
expected_str<std::unique_ptr<PluginSpec>> spec = checkPlugin( expected_str<std::unique_ptr<PluginSpec>> spec
readCppPluginSpec(m_data->sourcePath)); = checkPlugin(readCppPluginSpec(m_data->sourcePath), m_data->prepareForUpdate);
if (!spec) { if (!spec) {
m_label->setType(InfoLabel::Error); m_label->setType(InfoLabel::Error);
m_label->setText(spec.error()); m_label->setText(spec.error());
@@ -228,8 +236,18 @@ public:
return; return;
} }
m_tempDir = std::make_unique<TemporaryDirectory>("plugininstall"); FilePath tmpDirBase;
m_data->extractedPath = m_tempDir->path(); if (m_data->prepareForUpdate)
tmpDirBase = appInfo().userResources / "install-prep-area";
else
tmpDirBase = TemporaryDirectory::masterDirectoryFilePath();
tmpDirBase.ensureWritableDir();
m_tempDir = std::make_unique<QTemporaryDir>((tmpDirBase / "plugininstall").path());
m_tempDir->setAutoRemove(false);
m_data->extractedPath = FilePath::fromString(m_tempDir->path());
m_label->setText(Tr::tr("Checking archive...")); m_label->setText(Tr::tr("Checking archive..."));
m_label->setType(InfoLabel::None); m_label->setType(InfoLabel::None);
@@ -244,7 +262,7 @@ public:
const auto onUnarchiverSetup = [this, sourceAndCommand](Unarchiver &unarchiver) { const auto onUnarchiverSetup = [this, sourceAndCommand](Unarchiver &unarchiver) {
unarchiver.setSourceAndCommand(*sourceAndCommand); unarchiver.setSourceAndCommand(*sourceAndCommand);
unarchiver.setDestDir(m_tempDir->path()); unarchiver.setDestDir(m_data->extractedPath);
connect(&unarchiver, &Unarchiver::outputReceived, this, [this](const QString &output) { connect(&unarchiver, &Unarchiver::outputReceived, this, [this](const QString &output) {
m_output->append(output); m_output->append(output);
}); });
@@ -255,10 +273,11 @@ public:
}; };
const auto onCheckerSetup = [this](Async<CheckResult> &async) { const auto onCheckerSetup = [this](Async<CheckResult> &async) {
if (!m_tempDir) if (!m_data->extractedPath.exists())
return SetupResult::StopWithError; return SetupResult::StopWithError;
async.setConcurrentCallData(checkContents, m_tempDir->path()); async.setConcurrentCallData(
checkContents, m_data->extractedPath, m_data->prepareForUpdate);
return SetupResult::Continue; return SetupResult::Continue;
}; };
const auto onCheckerDone = [this](const Async<CheckResult> &async) { const auto onCheckerDone = [this](const Async<CheckResult> &async) {
@@ -289,8 +308,11 @@ public:
{ {
// back button pressed // back button pressed
m_taskTreeRunner.reset(); m_taskTreeRunner.reset();
if (m_tempDir) {
m_tempDir->remove();
m_tempDir.reset(); m_tempDir.reset();
} }
}
bool isComplete() const final { return m_isComplete; } bool isComplete() const final { return m_isComplete; }
@@ -301,7 +323,7 @@ public:
return WizardPage::nextId(); return WizardPage::nextId();
} }
std::unique_ptr<TemporaryDirectory> m_tempDir; std::unique_ptr<QTemporaryDir> m_tempDir;
TaskTreeRunner m_taskTreeRunner; TaskTreeRunner m_taskTreeRunner;
InfoLabel *m_label = nullptr; InfoLabel *m_label = nullptr;
QPushButton *m_cancelButton = nullptr; QPushButton *m_cancelButton = nullptr;
@@ -489,7 +511,13 @@ static bool copyPluginFile(const FilePath &src, const FilePath &dest)
return false; return false;
destFile.removeFile(); destFile.removeFile();
} }
dest.parentDir().ensureWritableDir(); if (!destFile.parentDir().ensureWritableDir()) {
QMessageBox::warning(
ICore::dialogParent(),
Tr::tr("Failed to Write File"),
Tr::tr("Failed to create directory \"%1\".").arg(dest.toUserOutput()));
return false;
}
if (!src.copyFile(destFile)) { if (!src.copyFile(destFile)) {
QMessageBox::warning(ICore::dialogParent(), QMessageBox::warning(ICore::dialogParent(),
Tr::tr("Failed to Write File"), Tr::tr("Failed to Write File"),
@@ -500,12 +528,18 @@ static bool copyPluginFile(const FilePath &src, const FilePath &dest)
return true; return true;
} }
InstallResult executePluginInstallWizard(const FilePath &archive) QString extensionId(PluginSpec *spec)
{
return spec->vendorId() + "." + spec->id();
}
InstallResult executePluginInstallWizard(const FilePath &archive, bool prepareForUpdate)
{ {
Wizard wizard; Wizard wizard;
wizard.setWindowTitle(Tr::tr("Install Plugin")); wizard.setWindowTitle(Tr::tr("Install Plugin"));
Data data; Data data;
data.prepareForUpdate = prepareForUpdate;
if (archive.isEmpty()) { if (archive.isEmpty()) {
auto filePage = new SourcePage(&data, &wizard); auto filePage = new SourcePage(&data, &wizard);
@@ -526,10 +560,23 @@ InstallResult executePluginInstallWizard(const FilePath &archive)
auto summaryPage = new SummaryPage(&data, &wizard); auto summaryPage = new SummaryPage(&data, &wizard);
wizard.addPage(summaryPage); wizard.addPage(summaryPage);
auto install = [&wizard, &data]() { auto install = [&wizard, &data, prepareForUpdate]() {
if (wizard.exec()) { if (wizard.exec()) {
const FilePath installPath = data.pluginSpec->installLocation( const FilePath installPath = data.pluginSpec->installLocation(
!data.installIntoApplication); !data.installIntoApplication)
/ extensionId(data.pluginSpec.get());
if (prepareForUpdate) {
if (hasLibSuffix(data.sourcePath)) {
ExtensionSystem::PluginManager::installPluginOnRestart(
data.pluginSpec->filePath(), installPath);
} else {
ExtensionSystem::PluginManager::installPluginOnRestart(
data.extractedPath, installPath);
}
return true;
}
if (hasLibSuffix(data.sourcePath)) { if (hasLibSuffix(data.sourcePath)) {
if (!copyPluginFile(data.sourcePath, installPath)) if (!copyPluginFile(data.sourcePath, installPath))
return false; return false;
@@ -568,6 +615,9 @@ InstallResult executePluginInstallWizard(const FilePath &archive)
// so we can safely set them as accepted here. // so we can safely set them as accepted here.
PluginManager::instance()->setTermsAndConditionsAccepted(data.pluginSpec.get()); PluginManager::instance()->setTermsAndConditionsAccepted(data.pluginSpec.get());
if (prepareForUpdate)
return InstallResult::NeedsRestart;
auto spec = data.pluginSpec.release(); auto spec = data.pluginSpec.release();
PluginManager::addPlugins({spec}); PluginManager::addPlugins({spec});

View File

@@ -17,6 +17,7 @@ enum class InstallResult {
NeedsRestart, NeedsRestart,
}; };
CORE_EXPORT InstallResult executePluginInstallWizard(const Utils::FilePath &archive = {}); CORE_EXPORT InstallResult
executePluginInstallWizard(const Utils::FilePath &archive = {}, bool prepareForUpdate = false);
} // namespace Core } // namespace Core

View File

@@ -25,6 +25,7 @@
#include <solutions/tasking/tasktreerunner.h> #include <solutions/tasking/tasktreerunner.h>
#include <utils/algorithm.h> #include <utils/algorithm.h>
#include <utils/appinfo.h>
#include <utils/fileutils.h> #include <utils/fileutils.h>
#include <utils/hostosinfo.h> #include <utils/hostosinfo.h>
#include <utils/icon.h> #include <utils/icon.h>
@@ -87,6 +88,21 @@ static QWidget *toScrollableColumn(QWidget *widget)
return scrollArea; return scrollArea;
}; };
const char kRestartSetting[] = "RestartAfterPluginEnabledChanged";
static void requestRestart()
{
if (ICore::infoBar()->canInfoBeAdded(kRestartSetting)) {
Utils::InfoBarEntry
info(kRestartSetting, Core::Tr::tr("Plugin changes will take effect after restart."));
info.addCustomButton(Tr::tr("Restart Now"), [] {
ICore::infoBar()->removeInfo(kRestartSetting);
QTimer::singleShot(0, ICore::instance(), &ICore::restart);
});
ICore::infoBar()->addInfo(info);
}
}
class CollapsingWidget : public QWidget class CollapsingWidget : public QWidget
{ {
public: public:
@@ -153,6 +169,24 @@ public:
installButton = new Button(Tr::tr("Install..."), Button::LargePrimary); installButton = new Button(Tr::tr("Install..."), Button::LargePrimary);
installButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); installButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
installButton->hide(); installButton->hide();
connect(
installButton,
&QAbstractButton::pressed,
this,
&HeadingWidget::pluginInstallationRequested);
removeButton = new Button(Tr::tr("Remove ..."), Button::SmallSecondary);
removeButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
removeButton->hide();
connect(removeButton, &QAbstractButton::pressed, this, [this]() {
ExtensionSystem::PluginManager::removePluginOnRestart(m_currentPluginId);
requestRestart();
});
updateButton = new Button(Tr::tr("Update ..."), Button::LargePrimary);
updateButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
updateButton->hide();
connect(updateButton, &QAbstractButton::pressed, this, &HeadingWidget::pluginUpdateRequested);
using namespace Layouting; using namespace Layouting;
Row { Row {
@@ -181,6 +215,8 @@ public:
}, },
Column { Column {
installButton, installButton,
updateButton,
removeButton,
st, st,
}, },
noMargin, spacing(SpacingTokens::ExPaddingGapL), noMargin, spacing(SpacingTokens::ExPaddingGapL),
@@ -189,8 +225,6 @@ public:
setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum); setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum);
m_dlCountItems->setVisible(false); m_dlCountItems->setVisible(false);
connect(installButton, &QAbstractButton::pressed,
this, &HeadingWidget::pluginInstallationRequested);
connect(m_vendor, &QAbstractButton::pressed, this, [this]() { connect(m_vendor, &QAbstractButton::pressed, this, [this]() {
emit vendorClicked(m_currentVendor); emit vendorClicked(m_currentVendor);
}); });
@@ -203,6 +237,8 @@ public:
if (!current.isValid()) if (!current.isValid())
return; return;
m_currentPluginId = current.data(RoleId).toString();
m_icon->setPixmap(itemIcon(current, SizeBig)); m_icon->setPixmap(itemIcon(current, SizeBig));
const QString name = current.data(RoleName).toString(); const QString name = current.data(RoleName).toString();
@@ -220,17 +256,27 @@ public:
const QString description = current.data(RoleDescriptionShort).toString(); const QString description = current.data(RoleDescriptionShort).toString();
m_details->setText(description); m_details->setText(description);
ExtensionSystem::PluginSpec *pluginSpec = pluginSpecForId(current.data(RoleId).toString());
const ItemType itemType = current.data(RoleItemType).value<ItemType>(); const ItemType itemType = current.data(RoleItemType).value<ItemType>();
const bool isPack = itemType == ItemTypePack; const bool isPack = itemType == ItemTypePack;
const bool isRemotePlugin = !(isPack || pluginSpecForId(current.data(RoleId).toString())); const bool isRemotePlugin = !(isPack || pluginSpec);
const QString downloadUrl = current.data(RoleDownloadUrl).toString(); const QString downloadUrl = current.data(RoleDownloadUrl).toString();
removeButton->setVisible(!isRemotePlugin && pluginSpec && !pluginSpec->isSystemPlugin());
installButton->setVisible(isRemotePlugin && !downloadUrl.isEmpty()); installButton->setVisible(isRemotePlugin && !downloadUrl.isEmpty());
if (installButton->isVisible()) if (installButton->isVisible())
installButton->setToolTip(downloadUrl); installButton->setToolTip(downloadUrl);
updateButton->setVisible(
pluginSpec
&& ExtensionSystem::PluginSpec::versionCompare(
pluginSpec->version(), current.data(RoleVersion).toString())
< 0);
} }
signals: signals:
void pluginInstallationRequested(); void pluginInstallationRequested();
void pluginUpdateRequested();
void vendorClicked(const QString &vendor); void vendorClicked(const QString &vendor);
private: private:
@@ -243,24 +289,12 @@ private:
QWidget *m_dlCountItems; QWidget *m_dlCountItems;
QLabel *m_details; QLabel *m_details;
QAbstractButton *installButton; QAbstractButton *installButton;
QAbstractButton *removeButton;
QAbstractButton *updateButton;
QString m_currentVendor; QString m_currentVendor;
QString m_currentPluginId;
}; };
const char kRestartSetting[] = "RestartAfterPluginEnabledChanged";
static void requestRestart()
{
if (ICore::infoBar()->canInfoBeAdded(kRestartSetting)) {
Utils::InfoBarEntry
info(kRestartSetting, Core::Tr::tr("Plugin changes will take effect after restart."));
info.addCustomButton(Tr::tr("Restart Now"), [] {
ICore::infoBar()->removeInfo(kRestartSetting);
QTimer::singleShot(0, ICore::instance(), &ICore::restart);
});
ICore::infoBar()->addInfo(info);
}
}
class PluginStatusWidget : public QWidget class PluginStatusWidget : public QWidget
{ {
public: public:
@@ -394,7 +428,7 @@ public:
private: private:
void updateView(const QModelIndex &current); void updateView(const QModelIndex &current);
void fetchAndInstallPlugin(const QUrl &url, const QString &id); void fetchAndInstallPlugin(const QUrl &url, const QString &id, bool update);
QString m_currentItemName; QString m_currentItemName;
ExtensionsModel *m_extensionModel; ExtensionsModel *m_extensionModel;
@@ -562,7 +596,10 @@ ExtensionManagerWidget::ExtensionManagerWidget()
m_secondaryDescriptionWidget->setWidth(secondaryDescriptionWidth); m_secondaryDescriptionWidget->setWidth(secondaryDescriptionWidth);
}); });
connect(m_headingWidget, &HeadingWidget::pluginInstallationRequested, this, [this]() { connect(m_headingWidget, &HeadingWidget::pluginInstallationRequested, this, [this]() {
fetchAndInstallPlugin(QUrl::fromUserInput(m_currentDownloadUrl), m_currentId); fetchAndInstallPlugin(QUrl::fromUserInput(m_currentDownloadUrl), m_currentId, false);
});
connect(m_headingWidget, &HeadingWidget::pluginUpdateRequested, this, [this]() {
fetchAndInstallPlugin(QUrl::fromUserInput(m_currentDownloadUrl), m_currentId, true);
}); });
connect(m_tags, &TagList::tagSelected, m_extensionBrowser, &ExtensionsBrowser::setFilter); connect(m_tags, &TagList::tagSelected, m_extensionBrowser, &ExtensionsBrowser::setFilter);
connect(m_headingWidget, &HeadingWidget::vendorClicked, connect(m_headingWidget, &HeadingWidget::vendorClicked,
@@ -649,7 +686,7 @@ void ExtensionManagerWidget::updateView(const QModelIndex &current)
} }
} }
void ExtensionManagerWidget::fetchAndInstallPlugin(const QUrl &url, const QString &id) void ExtensionManagerWidget::fetchAndInstallPlugin(const QUrl &url, const QString &id, bool update)
{ {
using namespace Tasking; using namespace Tasking;
@@ -720,7 +757,7 @@ void ExtensionManagerWidget::fetchAndInstallPlugin(const QUrl &url, const QStrin
} }
}; };
const auto onPluginInstallation = [storage]() { const auto onPluginInstallation = [storage, update]() {
if (storage->packageData.isEmpty()) if (storage->packageData.isEmpty())
return false; return false;
const FilePath source = FilePath::fromUrl(storage->url); const FilePath source = FilePath::fromUrl(storage->url);
@@ -730,7 +767,7 @@ void ExtensionManagerWidget::fetchAndInstallPlugin(const QUrl &url, const QStrin
saver.write(storage->packageData); saver.write(storage->packageData);
if (saver.finalize(ICore::dialogParent())) { if (saver.finalize(ICore::dialogParent())) {
auto result = executePluginInstallWizard(saver.filePath()); auto result = executePluginInstallWizard(saver.filePath(), update);
switch (result) { switch (result) {
case InstallResult::Success: case InstallResult::Success:
return true; return true;

View File

@@ -48,7 +48,7 @@ public:
void ExtensionsModelPrivate::addUnlistedLocalPlugins() void ExtensionsModelPrivate::addUnlistedLocalPlugins()
{ {
QStringList responseExtensions; QSet<QString> responseExtensions;
for (const QJsonValueConstRef &responseItem : qAsConst(responseItems)) for (const QJsonValueConstRef &responseItem : qAsConst(responseItems))
responseExtensions << responseItem.toObject().value("id").toString(); responseExtensions << responseItem.toObject().value("id").toString();
@@ -124,6 +124,8 @@ QVariant ExtensionsModelPrivate::dataFromRemotePlugin(const QJsonObject &json, i
} }
break; break;
} }
case RoleVersion:
return metaData.value("Version");
case RoleItemType: case RoleItemType:
return ItemTypeExtension; return ItemTypeExtension;
case RoleDescriptionLong: { case RoleDescriptionLong: {

View File

@@ -416,10 +416,26 @@ public:
FilePaths folders = path.dirEntries(FileFilter({}, QDir::Dirs | QDir::NoDotAndDotDot)); FilePaths folders = path.dirEntries(FileFilter({}, QDir::Dirs | QDir::NoDotAndDotDot));
for (const FilePath &folder : folders) { for (const FilePath &folder : folders) {
const FilePath script = folder / (folder.baseName() + ".lua"); FilePath script = folder / (folder.baseName() + ".lua");
if (!script.exists()) if (!script.exists()) {
FilePaths contents = folder.dirEntries(QDir::Dirs | QDir::NoDotAndDotDot);
if (contents.empty())
continue; continue;
for (const FilePath &subfolder : contents) {
script = subfolder / (subfolder.baseName() + ".lua");
if (!script.exists()) {
script.clear();
continue;
}
break;
}
}
if (script.isEmpty()) {
continue;
}
const expected_str<LuaPluginSpec *> result = loadPlugin(script); const expected_str<LuaPluginSpec *> result = loadPlugin(script);
if (!result) { if (!result) {
@@ -430,6 +446,19 @@ public:
continue; continue;
} }
if (PluginManager::takePluginIdForRemoval((*result)->id())) {
auto removeResult = (*result)->removePluginFiles();
if (!removeResult) {
qWarning() << "Failed to remove plugin files" << script << ":"
<< removeResult.error();
MessageManager::writeFlashing(
Tr::tr("Failed to remove plugin files of %1: %2")
.arg((*result)->id())
.arg(removeResult.error()));
}
continue;
}
plugins.insert(*result); plugins.insert(*result);
} }
} }