2 * Copyright (C) 2011-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
9 #include "AddonInstaller.h"
12 #include "FilesystemInstaller.h"
13 #include "GUIPassword.h"
14 #include "GUIUserMessages.h" // for callback
15 #include "ServiceBroker.h"
18 #include "addons/AddonManager.h"
19 #include "addons/AddonRepos.h"
20 #include "addons/Repository.h"
21 #include "addons/RepositoryUpdater.h"
22 #include "addons/addoninfo/AddonInfo.h"
23 #include "addons/addoninfo/AddonType.h"
24 #include "dialogs/GUIDialogExtendedProgressBar.h"
25 #include "events/AddonManagementEvent.h"
26 #include "events/EventLog.h"
27 #include "events/NotificationEvent.h"
28 #include "favourites/FavouritesService.h"
29 #include "filesystem/Directory.h"
30 #include "filesystem/File.h"
31 #include "guilib/GUIComponent.h"
32 #include "guilib/GUIWindowManager.h" // for callback
33 #include "guilib/LocalizeStrings.h"
34 #include "messaging/helpers/DialogHelper.h"
35 #include "messaging/helpers/DialogOKHelper.h"
36 #include "settings/AdvancedSettings.h"
37 #include "settings/Settings.h"
38 #include "settings/SettingsComponent.h"
39 #include "utils/FileOperationJob.h"
40 #include "utils/FileUtils.h"
41 #include "utils/JobManager.h"
42 #include "utils/StringUtils.h"
43 #include "utils/URIUtils.h"
44 #include "utils/Variant.h"
45 #include "utils/XTimeUtils.h"
46 #include "utils/log.h"
52 using namespace XFILE
;
53 using namespace ADDON
;
54 using namespace KODI::MESSAGING
;
56 using namespace std::chrono_literals
;
58 using KODI::MESSAGING::HELPERS::DialogResponse
;
59 using KODI::UTILITY::TypedDigest
;
63 class CAddonInstallJob
: public CFileOperationJob
66 CAddonInstallJob(const ADDON::AddonPtr
& addon
,
67 const ADDON::RepositoryPtr
& repo
,
68 AutoUpdateJob isAutoUpdate
);
70 bool DoWork() override
;
72 static constexpr const char* TYPE_DOWNLOAD
= "DOWNLOAD";
73 static constexpr const char* TYPE_INSTALL
= "INSTALL";
75 * \brief Returns the current processing type in the installation job
77 * \return The current processing type as string, can be \ref TYPE_DOWNLOAD or
80 const char* GetType() const override
{ return m_currentType
; }
82 /*! \brief Find the add-on and its repository for the given add-on ID
83 * \param addonID ID of the add-on to find
84 * \param[out] repo the repository to use
85 * \param[out] addon Add-on with the given add-on ID
86 * \return True if the add-on and its repository were found, false otherwise.
88 static bool GetAddon(const std::string
& addonID
,
89 ADDON::RepositoryPtr
& repo
,
90 ADDON::AddonPtr
& addon
);
92 void SetDependsInstall(DependencyJob dependsInstall
) { m_dependsInstall
= dependsInstall
; }
93 void SetAllowCheckForUpdates(AllowCheckForUpdates allowCheckForUpdates
)
95 m_allowCheckForUpdates
= allowCheckForUpdates
;
100 void OnPostInstall();
101 bool Install(const std::string
& installFrom
,
102 const ADDON::RepositoryPtr
& repo
= ADDON::RepositoryPtr());
103 bool DownloadPackage(const std::string
& path
, const std::string
& dest
);
105 bool DoFileOperation(FileAction action
,
106 CFileItemList
& items
,
107 const std::string
& file
,
108 bool useSameJob
= true);
110 /*! \brief Queue a notification for addon installation/update failure
111 \param addonID - addon id
112 \param fileName - filename which is shown in case the addon id is unknown
113 \param message - error message to be displayed
115 void ReportInstallError(const std::string
& addonID
,
116 const std::string
& fileName
,
117 const std::string
& message
= "");
119 ADDON::AddonPtr m_addon
;
120 ADDON::RepositoryPtr m_repo
;
122 AutoUpdateJob m_isAutoUpdate
;
123 DependencyJob m_dependsInstall
= DependencyJob::CHOICE_NO
;
124 AllowCheckForUpdates m_allowCheckForUpdates
= AllowCheckForUpdates::CHOICE_YES
;
125 const char* m_currentType
= TYPE_DOWNLOAD
;
128 class CAddonUnInstallJob
: public CFileOperationJob
131 CAddonUnInstallJob(const ADDON::AddonPtr
& addon
, bool removeData
);
133 bool DoWork() override
;
134 void SetRecurseOrphaned(RecurseOrphaned recurseOrphaned
) { m_recurseOrphaned
= recurseOrphaned
; };
137 void ClearFavourites();
139 ADDON::AddonPtr m_addon
;
141 RecurseOrphaned m_recurseOrphaned
= RecurseOrphaned::CHOICE_YES
;
144 } // unnamed namespace
146 CAddonInstaller::CAddonInstaller() : m_idle(true)
149 CAddonInstaller::~CAddonInstaller() = default;
151 CAddonInstaller
&CAddonInstaller::GetInstance()
153 static CAddonInstaller addonInstaller
;
154 return addonInstaller
;
157 void CAddonInstaller::OnJobComplete(unsigned int jobID
, bool success
, CJob
* job
)
159 std::unique_lock
<CCriticalSection
> lock(m_critSection
);
160 JobMap::iterator i
= find_if(m_downloadJobs
.begin(), m_downloadJobs
.end(), [jobID
](const std::pair
<std::string
, CDownloadJob
>& p
) {
161 return p
.second
.jobID
== jobID
;
163 if (i
!= m_downloadJobs
.end())
164 m_downloadJobs
.erase(i
);
165 if (m_downloadJobs
.empty())
170 CGUIMessage
msg(GUI_MSG_NOTIFY_ALL
, 0, 0, GUI_MSG_UPDATE
);
171 CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg
);
174 void CAddonInstaller::OnJobProgress(unsigned int jobID
, unsigned int progress
, unsigned int total
, const CJob
*job
)
176 std::unique_lock
<CCriticalSection
> lock(m_critSection
);
177 JobMap::iterator i
= find_if(m_downloadJobs
.begin(), m_downloadJobs
.end(), [jobID
](const std::pair
<std::string
, CDownloadJob
>& p
) {
178 return p
.second
.jobID
== jobID
;
180 if (i
!= m_downloadJobs
.end())
182 // update job progress
183 i
->second
.progress
= 100 / total
* progress
;
184 i
->second
.downloadFinshed
= std::string(job
->GetType()) == CAddonInstallJob::TYPE_INSTALL
;
185 CGUIMessage
msg(GUI_MSG_NOTIFY_ALL
, 0, 0, GUI_MSG_UPDATE_ITEM
);
186 msg
.SetStringParam(i
->first
);
188 CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg
);
192 bool CAddonInstaller::IsDownloading() const
194 std::unique_lock
<CCriticalSection
> lock(m_critSection
);
195 return !m_downloadJobs
.empty();
198 void CAddonInstaller::GetInstallList(VECADDONS
&addons
) const
200 std::unique_lock
<CCriticalSection
> lock(m_critSection
);
201 std::vector
<std::string
> addonIDs
;
202 for (JobMap::const_iterator i
= m_downloadJobs
.begin(); i
!= m_downloadJobs
.end(); ++i
)
205 addonIDs
.push_back(i
->first
);
209 auto& addonMgr
= CServiceBroker::GetAddonMgr();
210 for (const auto& addonId
: addonIDs
)
213 if (addonMgr
.FindInstallableById(addonId
, addon
))
214 addons
.emplace_back(std::move(addon
));
218 bool CAddonInstaller::GetProgress(const std::string
& addonID
, unsigned int& percent
, bool& downloadFinshed
) const
220 std::unique_lock
<CCriticalSection
> lock(m_critSection
);
221 JobMap::const_iterator i
= m_downloadJobs
.find(addonID
);
222 if (i
!= m_downloadJobs
.end())
224 percent
= i
->second
.progress
;
225 downloadFinshed
= i
->second
.downloadFinshed
;
231 bool CAddonInstaller::Cancel(const std::string
&addonID
)
233 std::unique_lock
<CCriticalSection
> lock(m_critSection
);
234 JobMap::iterator i
= m_downloadJobs
.find(addonID
);
235 if (i
!= m_downloadJobs
.end())
237 CServiceBroker::GetJobManager()->CancelJob(i
->second
.jobID
);
238 m_downloadJobs
.erase(i
);
239 if (m_downloadJobs
.empty())
247 bool CAddonInstaller::InstallModal(const std::string
& addonID
,
248 ADDON::AddonPtr
& addon
,
249 InstallModalPrompt promptForInstall
)
251 if (!g_passwordManager
.CheckMenuLock(WINDOW_ADDON_BROWSER
))
254 // we assume that addons that are enabled don't get to this routine (i.e. that GetAddon() has been called)
255 if (CServiceBroker::GetAddonMgr().GetAddon(addonID
, addon
, OnlyEnabled::CHOICE_NO
))
256 return false; // addon is installed but disabled, and the user has specifically activated something that needs
257 // the addon - should we enable it?
259 // check we have it available
260 if (!CServiceBroker::GetAddonMgr().FindInstallableById(addonID
, addon
))
263 // if specified ask the user if he wants it installed
264 if (promptForInstall
== InstallModalPrompt::CHOICE_YES
)
266 if (HELPERS::ShowYesNoDialogLines(CVariant
{24076}, CVariant
{24100}, CVariant
{addon
->Name()},
267 CVariant
{24101}) != DialogResponse::CHOICE_YES
)
273 if (!InstallOrUpdate(addonID
, BackgroundJob::CHOICE_NO
, ModalJob::CHOICE_YES
))
276 return CServiceBroker::GetAddonMgr().GetAddon(addonID
, addon
, OnlyEnabled::CHOICE_YES
);
280 bool CAddonInstaller::InstallOrUpdate(const std::string
& addonID
,
281 BackgroundJob background
,
286 if (!CAddonInstallJob::GetAddon(addonID
, repo
, addon
))
289 return DoInstall(addon
, repo
, background
, modal
, AutoUpdateJob::CHOICE_NO
,
290 DependencyJob::CHOICE_NO
, AllowCheckForUpdates::CHOICE_YES
);
293 bool CAddonInstaller::InstallOrUpdateDependency(const ADDON::AddonPtr
& dependsId
,
294 const ADDON::RepositoryPtr
& repo
)
296 return DoInstall(dependsId
, repo
, BackgroundJob::CHOICE_NO
, ModalJob::CHOICE_NO
,
297 AutoUpdateJob::CHOICE_NO
, DependencyJob::CHOICE_YES
,
298 AllowCheckForUpdates::CHOICE_YES
);
301 bool CAddonInstaller::RemoveDependency(const std::shared_ptr
<IAddon
>& dependsId
) const
303 const bool removeData
= CDirectory::Exists(dependsId
->Profile());
304 CAddonUnInstallJob
removeDependencyJob(dependsId
, removeData
);
305 removeDependencyJob
.SetRecurseOrphaned(RecurseOrphaned::CHOICE_NO
);
307 return removeDependencyJob
.DoWork();
310 std::vector
<std::string
> CAddonInstaller::RemoveOrphanedDepsRecursively() const
312 std::vector
<std::string
> removedItems
;
314 auto toRemove
= CServiceBroker::GetAddonMgr().GetOrphanedDependencies();
315 while (toRemove
.size() > 0)
317 for (const auto& dep
: toRemove
)
319 if (RemoveDependency(dep
))
321 removedItems
.emplace_back(dep
->Name()); // successfully removed
325 CLog::Log(LOGERROR
, "CAddonMgr::{}: failed to remove orphaned add-on/dependency: {}",
326 __func__
, dep
->Name());
330 toRemove
= CServiceBroker::GetAddonMgr().GetOrphanedDependencies();
336 bool CAddonInstaller::Install(const std::string
& addonId
,
337 const CAddonVersion
& version
,
338 const std::string
& repoId
)
340 CLog::Log(LOGDEBUG
, "CAddonInstaller: installing '{}' version '{}' from repository '{}'", addonId
,
341 version
.asString(), repoId
);
344 CAddonDatabase database
;
346 if (!database
.Open() || !database
.GetAddon(addonId
, version
, repoId
, addon
))
350 if (!CServiceBroker::GetAddonMgr().GetAddon(repoId
, repo
, AddonType::REPOSITORY
,
351 OnlyEnabled::CHOICE_YES
))
354 return DoInstall(addon
, std::static_pointer_cast
<CRepository
>(repo
), BackgroundJob::CHOICE_YES
,
355 ModalJob::CHOICE_NO
, AutoUpdateJob::CHOICE_NO
, DependencyJob::CHOICE_NO
,
356 AllowCheckForUpdates::CHOICE_YES
);
359 bool CAddonInstaller::DoInstall(const AddonPtr
& addon
,
360 const RepositoryPtr
& repo
,
361 BackgroundJob background
,
363 AutoUpdateJob autoUpdate
,
364 DependencyJob dependsInstall
,
365 AllowCheckForUpdates allowCheckForUpdates
)
367 // check whether we already have the addon installing
368 std::unique_lock
<CCriticalSection
> lock(m_critSection
);
369 if (m_downloadJobs
.find(addon
->ID()) != m_downloadJobs
.end())
372 CAddonInstallJob
* installJob
= new CAddonInstallJob(addon
, repo
, autoUpdate
);
373 if (background
== BackgroundJob::CHOICE_YES
)
375 // Workaround: because CAddonInstallJob is blocking waiting for other jobs, it needs to be run
376 // with priority dedicated.
378 CServiceBroker::GetJobManager()->AddJob(installJob
, this, CJob::PRIORITY_DEDICATED
);
379 m_downloadJobs
.insert(make_pair(addon
->ID(), CDownloadJob(jobID
)));
385 m_downloadJobs
.insert(make_pair(addon
->ID(), CDownloadJob(0)));
389 installJob
->SetDependsInstall(dependsInstall
);
390 installJob
->SetAllowCheckForUpdates(allowCheckForUpdates
);
393 if (modal
== ModalJob::CHOICE_YES
)
394 result
= installJob
->DoModal();
396 result
= installJob
->DoWork();
400 JobMap::iterator i
= m_downloadJobs
.find(addon
->ID());
401 m_downloadJobs
.erase(i
);
402 if (m_downloadJobs
.empty())
408 bool CAddonInstaller::InstallFromZip(const std::string
&path
)
410 if (!g_passwordManager
.CheckMenuLock(WINDOW_ADDON_BROWSER
))
413 CLog::Log(LOGDEBUG
, "CAddonInstaller: installing from zip '{}'", CURL::GetRedacted(path
));
415 // grab the descriptive XML document from the zip, and read it in
417 //! @bug some zip files return a single item (root folder) that we think is stored, so we don't use the zip:// protocol
418 CURL
pathToUrl(path
);
419 CURL zipDir
= URIUtils::CreateArchivePath("zip", pathToUrl
, "");
420 auto eventLog
= CServiceBroker::GetEventLog();
421 if (!CDirectory::GetDirectory(zipDir
, items
, "", DIR_FLAG_DEFAULTS
) ||
422 items
.Size() != 1 || !items
[0]->m_bIsFolder
)
425 eventLog
->AddWithNotification(EventPtr(
426 new CNotificationEvent(24045, StringUtils::Format(g_localizeStrings
.Get(24143), path
),
427 "special://xbmc/media/icon256x256.png", EventLevel::Error
)));
431 "CAddonInstaller: installing addon failed '{}' - itemsize: {}, first item is folder: {}",
432 CURL::GetRedacted(path
), items
.Size(), items
[0]->m_bIsFolder
);
437 if (CServiceBroker::GetAddonMgr().LoadAddonDescription(items
[0]->GetPath(), addon
))
438 return DoInstall(addon
, RepositoryPtr(), BackgroundJob::CHOICE_YES
, ModalJob::CHOICE_NO
,
439 AutoUpdateJob::CHOICE_NO
, DependencyJob::CHOICE_NO
,
440 AllowCheckForUpdates::CHOICE_YES
);
443 eventLog
->AddWithNotification(EventPtr(
444 new CNotificationEvent(24045, StringUtils::Format(g_localizeStrings
.Get(24143), path
),
445 "special://xbmc/media/icon256x256.png", EventLevel::Error
)));
449 bool CAddonInstaller::UnInstall(const AddonPtr
& addon
, bool removeData
)
451 CServiceBroker::GetJobManager()->AddJob(new CAddonUnInstallJob(addon
, removeData
), this);
455 bool CAddonInstaller::CheckDependencies(const AddonPtr
& addon
,
456 CAddonDatabase
* database
/* = nullptr */)
458 std::pair
<std::string
, std::string
> failedDep
;
459 return CheckDependencies(addon
, failedDep
, database
);
462 bool CAddonInstaller::CheckDependencies(const AddonPtr
& addon
,
463 std::pair
<std::string
, std::string
>& failedDep
,
464 CAddonDatabase
* database
/* = nullptr */)
466 std::vector
<std::string
> preDeps
;
467 preDeps
.push_back(addon
->ID());
468 CAddonDatabase localDB
;
472 return CheckDependencies(addon
, preDeps
, *database
, failedDep
);
475 bool CAddonInstaller::CheckDependencies(const AddonPtr
&addon
,
476 std::vector
<std::string
>& preDeps
, CAddonDatabase
&database
,
477 std::pair
<std::string
, std::string
> &failedDep
)
479 if (addon
== nullptr)
480 return true; // a nullptr addon has no dependencies
482 for (const auto& it
: addon
->GetDependencies())
484 const std::string
&addonID
= it
.id
;
485 const CAddonVersion
& versionMin
= it
.versionMin
;
486 const CAddonVersion
& version
= it
.version
;
487 bool optional
= it
.optional
;
489 const bool haveInstalledAddon
=
490 CServiceBroker::GetAddonMgr().GetAddon(addonID
, dep
, OnlyEnabled::CHOICE_NO
);
491 if ((haveInstalledAddon
&& !dep
->MeetsVersion(versionMin
, version
)) ||
492 (!haveInstalledAddon
&& !optional
))
494 // we have it but our version isn't good enough, or we don't have it and we need it
495 if (!CServiceBroker::GetAddonMgr().FindInstallableById(addonID
, dep
) ||
496 (dep
&& !dep
->MeetsVersion(versionMin
, version
)))
498 // we don't have it in a repo, or we have it but the version isn't good enough, so dep isn't satisfied.
499 CLog::Log(LOGDEBUG
, "CAddonInstallJob[{}]: requires {} version {} which is not available",
500 addon
->ID(), addonID
, version
.asString());
502 // fill in the details of the failed dependency
503 failedDep
.first
= addonID
;
504 failedDep
.second
= version
.asString();
510 // need to enable the dependency
511 if (dep
&& CServiceBroker::GetAddonMgr().IsAddonDisabled(addonID
) &&
512 !CServiceBroker::GetAddonMgr().EnableAddon(addonID
))
517 // at this point we have our dep, or the dep is optional (and we don't have it) so check that it's OK as well
518 //! @todo should we assume that installed deps are OK?
519 if (dep
&& std::find(preDeps
.begin(), preDeps
.end(), dep
->ID()) == preDeps
.end())
521 preDeps
.push_back(dep
->ID());
522 if (!CheckDependencies(dep
, preDeps
, database
, failedDep
))
534 bool CAddonInstaller::HasJob(const std::string
& ID
) const
536 std::unique_lock
<CCriticalSection
> lock(m_critSection
);
537 return m_downloadJobs
.find(ID
) != m_downloadJobs
.end();
540 void CAddonInstaller::PrunePackageCache()
542 std::map
<std::string
, std::unique_ptr
<CFileItemList
>> packs
;
543 int64_t size
= EnumeratePackageFolder(packs
);
544 int64_t limit
= static_cast<int64_t>(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_addonPackageFolderSize
) * 1024 * 1024;
549 // 1. Remove the largest packages, leaving at least 2 for each add-on
553 for (auto it
= packs
.begin(); it
!= packs
.end(); ++it
)
555 it
->second
->Sort(SortByLabel
, SortOrderDescending
);
556 for (int j
= 2; j
< it
->second
->Size(); j
++)
557 items
.Add(std::make_shared
<CFileItem
>(*it
->second
->Get(j
)));
560 items
.Sort(SortBySize
, SortOrderDescending
);
562 while (size
> limit
&& i
< items
.Size())
564 size
-= items
[i
]->m_dwSize
;
565 db
.RemovePackage(items
[i
]->GetPath());
566 CFileUtils::DeleteItem(items
[i
++]);
571 // 2. Remove the oldest packages (leaving least 1 for each add-on)
573 for (auto it
= packs
.begin(); it
!= packs
.end(); ++it
)
575 if (it
->second
->Size() > 1)
576 items
.Add(std::make_shared
<CFileItem
>(*it
->second
->Get(1)));
579 items
.Sort(SortByDate
, SortOrderAscending
);
581 while (size
> limit
&& i
< items
.Size())
583 size
-= items
[i
]->m_dwSize
;
584 db
.RemovePackage(items
[i
]->GetPath());
585 CFileUtils::DeleteItem(items
[i
++]);
590 void CAddonInstaller::InstallAddons(const VECADDONS
& addons
,
592 AllowCheckForUpdates allowCheckForUpdates
)
594 for (const auto& addon
: addons
)
598 if (CAddonInstallJob::GetAddon(addon
->ID(), repo
, toInstall
))
599 DoInstall(toInstall
, repo
, BackgroundJob::CHOICE_NO
, ModalJob::CHOICE_NO
,
600 AutoUpdateJob::CHOICE_YES
, DependencyJob::CHOICE_NO
, allowCheckForUpdates
);
604 std::unique_lock
<CCriticalSection
> lock(m_critSection
);
605 if (!m_downloadJobs
.empty())
614 int64_t CAddonInstaller::EnumeratePackageFolder(
615 std::map
<std::string
, std::unique_ptr
<CFileItemList
>>& result
)
618 CDirectory::GetDirectory("special://home/addons/packages/",items
,".zip",DIR_FLAG_NO_FILE_DIRS
);
620 for (int i
= 0; i
< items
.Size(); i
++)
622 if (items
[i
]->m_bIsFolder
)
625 size
+= items
[i
]->m_dwSize
;
626 std::string pack
,dummy
;
627 CAddonVersion::SplitFileName(pack
, dummy
, items
[i
]->GetLabel());
628 if (result
.find(pack
) == result
.end())
629 result
[pack
] = std::make_unique
<CFileItemList
>();
630 result
[pack
]->Add(std::make_shared
<CFileItem
>(*items
[i
]));
636 CAddonInstallJob::CAddonInstallJob(const AddonPtr
& addon
,
637 const RepositoryPtr
& repo
,
638 AutoUpdateJob isAutoUpdate
)
639 : m_addon(addon
), m_repo(repo
), m_isAutoUpdate(isAutoUpdate
)
642 m_isUpdate
= CServiceBroker::GetAddonMgr().GetAddon(addon
->ID(), dummy
, OnlyEnabled::CHOICE_NO
);
645 bool CAddonInstallJob::GetAddon(const std::string
& addonID
, RepositoryPtr
& repo
,
646 ADDON::AddonPtr
& addon
)
648 if (!CServiceBroker::GetAddonMgr().FindInstallableById(addonID
, addon
))
652 if (!CServiceBroker::GetAddonMgr().GetAddon(addon
->Origin(), tmp
, AddonType::REPOSITORY
,
653 OnlyEnabled::CHOICE_YES
))
656 repo
= std::static_pointer_cast
<CRepository
>(tmp
);
661 bool CAddonInstallJob::DoWork()
663 m_currentType
= CAddonInstallJob::TYPE_DOWNLOAD
;
665 SetTitle(StringUtils::Format(g_localizeStrings
.Get(24057), m_addon
->Name()));
668 // check whether all the dependencies are available or not
669 SetText(g_localizeStrings
.Get(24058));
670 std::pair
<std::string
, std::string
> failedDep
;
671 if (!CAddonInstaller::GetInstance().CheckDependencies(m_addon
, failedDep
))
673 std::string details
=
674 StringUtils::Format(g_localizeStrings
.Get(24142), failedDep
.first
, failedDep
.second
);
675 CLog::Log(LOGERROR
, "CAddonInstallJob[{}]: {}", m_addon
->ID(), details
);
676 ReportInstallError(m_addon
->ID(), m_addon
->ID(), details
);
680 std::string installFrom
;
682 // Addons are installed by downloading the .zip package on the server to the local
683 // packages folder, then extracting from the local .zip package into the addons folder
684 // Both these functions are achieved by "copying" using the vfs.
686 if (!m_repo
&& URIUtils::HasSlashAtEnd(m_addon
->Path()))
687 { // passed in a folder - all we need do is copy it across
688 installFrom
= m_addon
->Path();
692 std::string path
{m_addon
->Path()};
696 CRepository::ResolveResult resolvedAddon
= m_repo
->ResolvePathAndHash(m_addon
);
697 path
= resolvedAddon
.location
;
698 hash
= resolvedAddon
.digest
;
701 CLog::Log(LOGERROR
, "CAddonInstallJob[{}]: failed to resolve addon install source path",
703 ReportInstallError(m_addon
->ID(), m_addon
->ID());
711 CLog::Log(LOGERROR
, "CAddonInstallJob[{}]: failed to open database", m_addon
->ID());
712 ReportInstallError(m_addon
->ID(), m_addon
->ID());
716 std::string packageOriginalPath
, packageFileName
;
717 URIUtils::Split(path
, packageOriginalPath
, packageFileName
);
718 // Use ChangeBasePath so the URL is decoded if necessary
719 const std::string packagePath
= "special://home/addons/packages/";
720 //!@todo fix design flaw in file copying: We use CFileOperationJob to download the package from the internet
721 // to the local cache. It tries to be "smart" and decode the URL. But it never tells us what the result is,
722 // so if we try for example to download "http://localhost/a+b.zip" the result ends up in "a b.zip".
723 // First bug is that it actually decodes "+", which is not necessary except in query parts. Second bug
724 // is that we cannot know that it does this and what the result is so the package will not be found without
725 // using ChangeBasePath here (which is the same function the copying code uses and performs the translation).
726 std::string package
= URIUtils::ChangeBasePath(packageOriginalPath
, packageFileName
, packagePath
);
728 // check that we don't already have a valid copy
731 std::string hashExisting
;
732 if (db
.GetPackageHash(m_addon
->ID(), package
, hashExisting
) && hash
.value
!= hashExisting
)
734 db
.RemovePackage(package
);
736 if (CFile::Exists(package
))
738 CFile::Delete(package
);
742 // zip passed in - download + extract
743 if (!CFile::Exists(package
))
745 if (!DownloadPackage(path
, packagePath
))
747 CFile::Delete(package
);
749 CLog::Log(LOGERROR
, "CAddonInstallJob[{}]: failed to download {}", m_addon
->ID(),
751 ReportInstallError(m_addon
->ID(), URIUtils::GetFileName(package
));
756 // at this point we have the package - check that it is valid
757 SetText(g_localizeStrings
.Get(24077));
760 TypedDigest actualHash
{hash
.type
, CUtil::GetFileDigest(package
, hash
.type
)};
761 if (hash
!= actualHash
)
763 CFile::Delete(package
);
765 CLog::Log(LOGERROR
, "CAddonInstallJob[{}]: Hash mismatch after download. Expected {}, was {}",
766 m_addon
->ID(), hash
.value
, actualHash
.value
);
767 ReportInstallError(m_addon
->ID(), URIUtils::GetFileName(package
));
771 db
.AddPackage(m_addon
->ID(), package
, hash
.value
);
774 // check if the archive is valid
775 CURL archive
= URIUtils::CreateArchivePath("zip", CURL(package
), "");
777 CFileItemList archivedFiles
;
779 if (!CDirectory::GetDirectory(archive
, archivedFiles
, "", DIR_FLAG_DEFAULTS
) ||
780 archivedFiles
.Size() != 1 || !archivedFiles
[0]->m_bIsFolder
||
781 !CServiceBroker::GetAddonMgr().LoadAddonDescription(archivedFiles
[0]->GetPath(), temp
))
783 CLog::Log(LOGERROR
, "CAddonInstallJob[{}]: invalid package {}", m_addon
->ID(), package
);
784 db
.RemovePackage(package
);
785 CFile::Delete(package
);
786 ReportInstallError(m_addon
->ID(), URIUtils::GetFileName(package
));
790 installFrom
= package
;
794 m_currentType
= CAddonInstallJob::TYPE_INSTALL
;
796 // run any pre-install functions
797 ADDON::OnPreInstall(m_addon
);
799 if (!CServiceBroker::GetAddonMgr().UnloadAddon(m_addon
->ID()))
801 CLog::Log(LOGERROR
, "CAddonInstallJob[{}]: failed to unload addon.", m_addon
->ID());
806 if (!Install(installFrom
, m_repo
))
809 // Load new installed and if successed replace defined m_addon here with new one
810 if (!CServiceBroker::GetAddonMgr().LoadAddon(m_addon
->ID(), m_addon
->Origin(),
811 m_addon
->Version()) ||
812 !CServiceBroker::GetAddonMgr().GetAddon(m_addon
->ID(), m_addon
, OnlyEnabled::CHOICE_YES
))
814 CLog::Log(LOGERROR
, "CAddonInstallJob[{}]: failed to reload addon", m_addon
->ID());
818 g_localizeStrings
.LoadAddonStrings(URIUtils::AddFileToFolder(m_addon
->Path(), "resources/language/"),
819 CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_LANGUAGE
), m_addon
->ID());
821 ADDON::OnPostInstall(m_addon
, m_isUpdate
, IsModal());
823 // Write origin to database via addon manager, where this information is up-to-date.
824 // Needed to set origin correctly for new installed addons.
827 if (m_addon
->Origin() == ORIGIN_SYSTEM
)
829 origin
= ORIGIN_SYSTEM
; // keep system add-on origin as ORIGIN_SYSTEM
831 else if (m_addon
->HasMainType(AddonType::REPOSITORY
))
833 origin
= m_addon
->ID(); // use own id as origin if repository
835 // if a repository is updated during the add-on migration process, we need to skip
836 // calling CheckForUpdates() on the repo to prevent deadlock issues during migration
838 if (m_allowCheckForUpdates
== AllowCheckForUpdates::CHOICE_YES
)
842 CLog::Log(LOGDEBUG
, "ADDONS: repository [{}] updated. now checking for content updates.",
844 CServiceBroker::GetRepositoryUpdater().CheckForUpdates(
845 std::static_pointer_cast
<CRepository
>(m_addon
), false);
850 CLog::Log(LOGDEBUG
, "ADDONS: skipping CheckForUpdates() on repository [{}].", m_addon
->ID());
855 origin
= m_repo
->ID(); // use repo id as origin
858 CServiceBroker::GetAddonMgr().SetAddonOrigin(m_addon
->ID(), origin
, m_isUpdate
);
860 if (m_dependsInstall
== DependencyJob::CHOICE_YES
)
862 CLog::Log(LOGDEBUG
, "ADDONS: dependency [{}] will not be version checked and unpinned",
867 // we only do pinning/unpinning for non-system add-ons
868 if (m_addon
->Origin() != ORIGIN_SYSTEM
)
870 // get all compatible versions of an addon-id regardless of their origin
871 // from all installed repositories
872 std::vector
<std::shared_ptr
<IAddon
>> compatibleVersions
=
873 CServiceBroker::GetAddonMgr().GetCompatibleVersions(m_addon
->ID());
875 if (!m_addon
->Origin().empty())
877 // handle add-ons that originate from a repository
879 // find the latest version for the origin we installed from
880 CAddonVersion latestVersion
;
881 for (const auto& compatibleVersion
: compatibleVersions
)
883 if (compatibleVersion
->Origin() == m_addon
->Origin() &&
884 compatibleVersion
->Version() > latestVersion
)
886 latestVersion
= compatibleVersion
->Version();
890 if (m_addon
->Version() == latestVersion
)
892 // unpin the installed addon if it's the latest of its origin
893 CServiceBroker::GetAddonMgr().RemoveUpdateRuleFromList(m_addon
->ID(),
894 AddonUpdateRule::PIN_OLD_VERSION
);
895 CLog::Log(LOGDEBUG
, "ADDONS: unpinned Addon: [{}] Origin: [{}] Version: [{}]",
896 m_addon
->ID(), m_addon
->Origin(), m_addon
->Version().asString());
900 // pin if it is not the latest
901 CServiceBroker::GetAddonMgr().AddUpdateRuleToList(m_addon
->ID(),
902 AddonUpdateRule::PIN_OLD_VERSION
);
903 CLog::Log(LOGDEBUG
, "ADDONS: pinned Addon: [{}] Origin: [{}] Version: [{}]",
904 m_addon
->ID(), m_addon
->Origin(), m_addon
->Version().asString());
909 // handle manually installed add-ons
911 // find the latest version of any origin/repository
912 CAddonVersion latestVersion
;
913 for (const auto& compatibleVersion
: compatibleVersions
)
915 if (compatibleVersion
->Version() > latestVersion
)
917 latestVersion
= compatibleVersion
->Version();
921 if (m_addon
->Version() < latestVersion
)
923 // pin zip version if it's lesser than latest from repo(s)
924 CServiceBroker::GetAddonMgr().AddUpdateRuleToList(m_addon
->ID(),
925 AddonUpdateRule::PIN_ZIP_INSTALL
);
926 CLog::Log(LOGDEBUG
, "ADDONS: pinned zip installed Addon: [{}] Version: [{}]",
927 m_addon
->ID(), m_addon
->Version().asString());
931 // unpin zip version if it's >= the latest from repos
932 CServiceBroker::GetAddonMgr().RemoveUpdateRuleFromList(m_addon
->ID(),
933 AddonUpdateRule::PIN_ZIP_INSTALL
);
934 CLog::Log(LOGDEBUG
, "ADDONS: unpinned zip installed Addon: [{}] Version: [{}]",
935 m_addon
->ID(), m_addon
->Version().asString());
941 bool notify
= (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
942 CSettings::SETTING_ADDONS_NOTIFICATIONS
) ||
943 m_isAutoUpdate
== AutoUpdateJob::CHOICE_NO
) &&
944 !IsModal() && m_dependsInstall
== DependencyJob::CHOICE_NO
;
945 auto eventLog
= CServiceBroker::GetEventLog();
947 eventLog
->Add(EventPtr(new CAddonManagementEvent(m_addon
, m_isUpdate
? 24065 : 24084)), notify
,
950 if (m_isAutoUpdate
== AutoUpdateJob::CHOICE_YES
&&
951 m_addon
->LifecycleState() == AddonLifecycleState::BROKEN
)
953 CLog::Log(LOGDEBUG
, "CAddonInstallJob[{}]: auto-disabling due to being marked as broken",
955 CServiceBroker::GetAddonMgr().DisableAddon(m_addon
->ID(), AddonDisabledReason::USER
);
957 eventLog
->Add(EventPtr(new CAddonManagementEvent(m_addon
, 24094)), true, false);
959 else if (m_addon
->LifecycleState() == AddonLifecycleState::DEPRECATED
)
961 CLog::Log(LOGDEBUG
, "CAddonInstallJob[{}]: installed addon marked as deprecated",
964 StringUtils::Format(g_localizeStrings
.Get(24168), m_addon
->LifecycleStateDescription());
966 eventLog
->Add(EventPtr(new CAddonManagementEvent(m_addon
, text
)), true, false);
974 bool CAddonInstallJob::DownloadPackage(const std::string
&path
, const std::string
&dest
)
976 if (ShouldCancel(0, 1))
979 SetText(g_localizeStrings
.Get(24078));
981 // need to download/copy the package first
983 list
.Add(std::make_shared
<CFileItem
>(path
, false));
984 list
[0]->Select(true);
986 return DoFileOperation(CFileOperationJob::ActionReplace
, list
, dest
, true);
989 bool CAddonInstallJob::DoFileOperation(FileAction action
, CFileItemList
&items
, const std::string
&file
, bool useSameJob
/* = true */)
994 SetFileOperation(action
, items
, file
);
996 // temporarily disable auto-closing so not to close the current progress indicator
997 bool autoClose
= GetAutoClose();
1000 // temporarily disable updating title or text
1001 bool updateInformation
= GetUpdateInformation();
1002 if (updateInformation
)
1003 SetUpdateInformation(false);
1005 result
= CFileOperationJob::DoWork();
1007 SetUpdateInformation(updateInformation
);
1008 SetAutoClose(autoClose
);
1012 CFileOperationJob
job(action
, items
, file
);
1014 // pass our progress indicators to the temporary job and only allow it to
1015 // show progress updates (no title or text changes)
1016 job
.SetProgressIndicators(GetProgressBar(), GetProgressDialog(), GetUpdateProgress(), false);
1018 result
= job
.DoWork();
1024 bool CAddonInstallJob::Install(const std::string
&installFrom
, const RepositoryPtr
& repo
)
1026 const auto& deps
= m_addon
->GetDependencies();
1028 if (!deps
.empty() && m_addon
->HasType(AddonType::REPOSITORY
))
1030 bool notSystemAddon
= std::none_of(deps
.begin(), deps
.end(), [](const DependencyInfo
& dep
) {
1031 return CServiceBroker::GetAddonMgr().IsSystemAddon(dep
.id
);
1038 "CAddonInstallJob::{}: failed to install repository [{}]. It has dependencies defined",
1039 __func__
, m_addon
->ID());
1040 ReportInstallError(m_addon
->ID(), m_addon
->ID(), g_localizeStrings
.Get(24088));
1045 SetText(g_localizeStrings
.Get(24079));
1046 unsigned int totalSteps
= static_cast<unsigned int>(deps
.size()) + 1;
1047 if (ShouldCancel(0, totalSteps
))
1050 CAddonRepos addonRepos
;
1051 if (!addonRepos
.IsValid())
1054 // The first thing we do is install dependencies
1055 for (auto it
= deps
.begin(); it
!= deps
.end(); ++it
)
1057 if (it
->id
!= "xbmc.metadata")
1059 const std::string
&addonID
= it
->id
;
1060 const CAddonVersion
& versionMin
= it
->versionMin
;
1061 const CAddonVersion
& version
= it
->version
;
1062 bool optional
= it
->optional
;
1063 AddonPtr dependency
;
1064 const bool haveInstalledAddon
=
1065 CServiceBroker::GetAddonMgr().GetAddon(addonID
, dependency
, OnlyEnabled::CHOICE_NO
);
1066 if ((haveInstalledAddon
&& !dependency
->MeetsVersion(versionMin
, version
)) ||
1067 (!haveInstalledAddon
&& !optional
))
1069 // we have it but our version isn't good enough, or we don't have it and we need it
1071 // dependency is already queued up for install - ::Install will fail
1072 // instead we wait until the Job has finished. note that we
1073 // recall install on purpose in case prior installation failed
1074 if (CAddonInstaller::GetInstance().HasJob(addonID
))
1076 while (CAddonInstaller::GetInstance().HasJob(addonID
))
1077 KODI::TIME::Sleep(50ms
);
1079 if (!CServiceBroker::GetAddonMgr().IsAddonInstalled(addonID
))
1081 CLog::Log(LOGERROR
, "CAddonInstallJob[{}]: failed to install dependency {}",
1082 m_addon
->ID(), addonID
);
1083 ReportInstallError(m_addon
->ID(), m_addon
->ID(), g_localizeStrings
.Get(24085));
1087 // don't have the addon or the addon isn't new enough - grab it (no new job for these)
1090 RepositoryPtr repoForDep
;
1091 AddonPtr dependencyToInstall
;
1093 // origin of m_addon is empty at least if an addon is installed for the first time
1094 // we need to override "parentRepoId" if the passed in repo is valid.
1096 const std::string
& parentRepoId
=
1097 m_addon
->Origin().empty() && repo
? repo
->ID() : m_addon
->Origin();
1099 if (!addonRepos
.FindDependency(addonID
, parentRepoId
, dependencyToInstall
, repoForDep
))
1101 CLog::Log(LOGERROR
, "CAddonInstallJob[{}]: failed to find dependency {}", m_addon
->ID(),
1103 ReportInstallError(m_addon
->ID(), m_addon
->ID(), g_localizeStrings
.Get(24085));
1108 if (!dependencyToInstall
->MeetsVersion(versionMin
, version
))
1111 "CAddonInstallJob[{}]: found dependency [{}/{}] doesn't meet minimum "
1113 m_addon
->ID(), addonID
, dependencyToInstall
->Version().asString(),
1114 versionMin
.asString());
1115 ReportInstallError(m_addon
->ID(), m_addon
->ID(), g_localizeStrings
.Get(24085));
1122 CAddonInstallJob
dependencyJob(dependencyToInstall
, repoForDep
,
1123 AutoUpdateJob::CHOICE_NO
);
1124 dependencyJob
.SetDependsInstall(DependencyJob::CHOICE_YES
);
1126 // pass our progress indicators to the temporary job and don't allow it to
1127 // show progress or information updates (no progress, title or text changes)
1128 dependencyJob
.SetProgressIndicators(GetProgressBar(), GetProgressDialog(), false,
1131 if (!dependencyJob
.DoModal())
1133 CLog::Log(LOGERROR
, "CAddonInstallJob[{}]: failed to install dependency {}",
1134 m_addon
->ID(), addonID
);
1135 ReportInstallError(m_addon
->ID(), m_addon
->ID(), g_localizeStrings
.Get(24085));
1139 else if (!CAddonInstaller::GetInstance().InstallOrUpdateDependency(dependencyToInstall
,
1142 CLog::Log(LOGERROR
, "CAddonInstallJob[{}]: failed to install dependency {}",
1143 m_addon
->ID(), dependencyToInstall
->ID());
1144 ReportInstallError(m_addon
->ID(), m_addon
->ID(), g_localizeStrings
.Get(24085));
1151 if (ShouldCancel(std::distance(deps
.begin(), it
), totalSteps
))
1155 SetText(g_localizeStrings
.Get(24086));
1156 SetProgress(static_cast<unsigned int>(100.0 * (totalSteps
- 1.0) / totalSteps
));
1158 CFilesystemInstaller fsInstaller
;
1159 if (!fsInstaller
.InstallToFilesystem(installFrom
, m_addon
->ID()))
1161 ReportInstallError(m_addon
->ID(), m_addon
->ID());
1170 void CAddonInstallJob::ReportInstallError(const std::string
& addonID
, const std::string
& fileName
, const std::string
& message
/* = "" */)
1173 CServiceBroker::GetAddonMgr().FindInstallableById(addonID
, addon
);
1177 std::string msg
= message
;
1179 if (addon
!= nullptr)
1182 bool success
= CServiceBroker::GetAddonMgr().GetAddon(addonID
, addon2
, OnlyEnabled::CHOICE_YES
);
1185 msg
= g_localizeStrings
.Get(addon2
!= nullptr && success
? 113 : 114);
1188 activity
= EventPtr(new CAddonManagementEvent(addon
, EventLevel::Error
, msg
));
1190 HELPERS::ShowOKDialogText(CVariant
{m_addon
->Name()}, CVariant
{msg
});
1194 activity
= EventPtr(new CNotificationEvent(
1195 24045, !msg
.empty() ? msg
: StringUtils::Format(g_localizeStrings
.Get(24143), fileName
),
1196 EventLevel::Error
));
1199 HELPERS::ShowOKDialogText(CVariant
{fileName
}, CVariant
{msg
});
1202 auto eventLog
= CServiceBroker::GetEventLog();
1204 eventLog
->Add(activity
, !IsModal(), false);
1207 CAddonUnInstallJob::CAddonUnInstallJob(const AddonPtr
&addon
, bool removeData
)
1208 : m_addon(addon
), m_removeData(removeData
)
1211 bool CAddonUnInstallJob::DoWork()
1213 ADDON::OnPreUnInstall(m_addon
);
1215 //Unregister addon with the manager to ensure nothing tries
1216 //to interact with it while we are uninstalling.
1217 if (!CServiceBroker::GetAddonMgr().UnloadAddon(m_addon
->ID()))
1219 CLog::Log(LOGERROR
, "CAddonUnInstallJob[{}]: failed to unload addon.", m_addon
->ID());
1223 CFilesystemInstaller fsInstaller
;
1224 if (!fsInstaller
.UnInstallFromFilesystem(m_addon
->Path()))
1226 CLog::Log(LOGERROR
, "CAddonUnInstallJob[{}]: could not delete addon data.", m_addon
->ID());
1233 CFileUtils::DeleteItem(m_addon
->Profile());
1238 // try to get the addon object from the repository as the local one does not exist anymore
1239 // if that doesn't work fall back to the local one
1240 if (!CServiceBroker::GetAddonMgr().FindInstallableById(m_addon
->ID(), addon
) || addon
== nullptr)
1245 auto eventLog
= CServiceBroker::GetEventLog();
1247 eventLog
->Add(EventPtr(new CAddonManagementEvent(addon
, 24144))); // Add-on uninstalled
1249 CServiceBroker::GetAddonMgr().OnPostUnInstall(m_addon
->ID());
1251 CAddonDatabase database
;
1252 if (database
.Open())
1253 database
.OnPostUnInstall(m_addon
->ID());
1255 ADDON::OnPostUnInstall(m_addon
);
1257 if (m_recurseOrphaned
== RecurseOrphaned::CHOICE_YES
)
1259 const auto removedItems
= CAddonInstaller::GetInstance().RemoveOrphanedDepsRecursively();
1261 if (removedItems
.size() > 0)
1263 CLog::Log(LOGINFO
, "CAddonUnInstallJob[{}]: removed orphaned dependencies ({})",
1264 m_addon
->ID(), StringUtils::Join(removedItems
, ", "));
1271 void CAddonUnInstallJob::ClearFavourites()
1274 CFileItemList items
;
1275 CServiceBroker::GetFavouritesService().GetAll(items
);
1276 for (int i
= 0; i
< items
.Size(); i
++)
1278 if (items
[i
]->GetPath().find(m_addon
->ID()) != std::string::npos
)
1280 items
.Remove(items
[i
].get());
1286 CServiceBroker::GetFavouritesService().Save(items
);