pkgman: Refactoring -> PackageManager
Move common and reusable functionality from "search" and "install" to new PackageManager class.
This commit is contained in:
@@ -14,6 +14,7 @@ BinCommand pkgman :
|
|||||||
DecisionProvider.cpp
|
DecisionProvider.cpp
|
||||||
JobStateListener.cpp
|
JobStateListener.cpp
|
||||||
PackageInfoErrorListener.cpp
|
PackageInfoErrorListener.cpp
|
||||||
|
PackageManager.cpp
|
||||||
pkgman.cpp
|
pkgman.cpp
|
||||||
RepositoryBuilder.cpp
|
RepositoryBuilder.cpp
|
||||||
:
|
:
|
||||||
|
|||||||
@@ -0,0 +1,396 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013, Haiku, Inc. All Rights Reserved.
|
||||||
|
* Distributed under the terms of the MIT License.
|
||||||
|
*
|
||||||
|
* Authors:
|
||||||
|
* Ingo Weinhold <[email protected]>
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#include "PackageManager.h"
|
||||||
|
|
||||||
|
#include <Directory.h>
|
||||||
|
#include <package/DownloadFileRequest.h>
|
||||||
|
#include <package/PackageRoster.h>
|
||||||
|
#include <package/RefreshRepositoryRequest.h>
|
||||||
|
#include <package/solver/SolverPackage.h>
|
||||||
|
#include <package/solver/SolverPackageSpecifier.h>
|
||||||
|
#include <package/solver/SolverPackageSpecifierList.h>
|
||||||
|
#include <package/solver/SolverProblem.h>
|
||||||
|
#include <package/solver/SolverProblemSolution.h>
|
||||||
|
#include <package/solver/SolverResult.h>
|
||||||
|
|
||||||
|
#include <package/ActivationTransaction.h>
|
||||||
|
#include <package/DaemonClient.h>
|
||||||
|
|
||||||
|
#include "pkgman.h"
|
||||||
|
#include "RepositoryBuilder.h"
|
||||||
|
|
||||||
|
|
||||||
|
using namespace BPackageKit::BPrivate;
|
||||||
|
|
||||||
|
|
||||||
|
// #pragma mark - Repository
|
||||||
|
|
||||||
|
|
||||||
|
PackageManager::Repository::Repository()
|
||||||
|
:
|
||||||
|
BSolverRepository()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
status_t
|
||||||
|
PackageManager::Repository::Init(BPackageRoster& roster, BContext& context,
|
||||||
|
const char* name)
|
||||||
|
{
|
||||||
|
// get the repository config
|
||||||
|
status_t error = roster.GetRepositoryConfig(name, &fConfig);
|
||||||
|
if (error != B_OK)
|
||||||
|
return error;
|
||||||
|
|
||||||
|
// refresh
|
||||||
|
BRefreshRepositoryRequest refreshRequest(context, fConfig);
|
||||||
|
error = refreshRequest.Process();
|
||||||
|
if (error != B_OK) {
|
||||||
|
WARN(error, "refreshing repository \"%s\" failed", name);
|
||||||
|
return B_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-get the config
|
||||||
|
return roster.GetRepositoryConfig(name, &fConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const BRepositoryConfig&
|
||||||
|
PackageManager::Repository::Config() const
|
||||||
|
{
|
||||||
|
return fConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// #pragma mark - Solver
|
||||||
|
|
||||||
|
|
||||||
|
PackageManager::PackageManager(BPackageInstallationLocation location,
|
||||||
|
bool addInstalledRepositories, bool addOtherRepositories)
|
||||||
|
:
|
||||||
|
fLocation(location),
|
||||||
|
fSolver(NULL),
|
||||||
|
fSystemRepository(),
|
||||||
|
fCommonRepository(),
|
||||||
|
fHomeRepository(),
|
||||||
|
fInstalledRepositories(10),
|
||||||
|
fOtherRepositories(10, true),
|
||||||
|
fDecisionProvider(),
|
||||||
|
fJobStateListener(),
|
||||||
|
fContext(fDecisionProvider, fJobStateListener)
|
||||||
|
{
|
||||||
|
// create the solver
|
||||||
|
status_t error = BSolver::Create(fSolver);
|
||||||
|
if (error != B_OK)
|
||||||
|
DIE(error, "failed to create solver");
|
||||||
|
|
||||||
|
// add installation location repositories
|
||||||
|
if (addInstalledRepositories) {
|
||||||
|
// We add only the repository of our actual installation location as the
|
||||||
|
// "installed" repository. The repositories for the more general
|
||||||
|
// installation locations are added as regular repositories, but with
|
||||||
|
// better priorities than the actual (remote) repositories. This
|
||||||
|
// prevents the solver from showing conflicts when a package in a more
|
||||||
|
// specific installation location overrides a package in a more general
|
||||||
|
// one. Instead any requirement that is already installed in a more
|
||||||
|
// general installation location will turn up as to be installed as
|
||||||
|
// well. But we can easily filter those out.
|
||||||
|
RepositoryBuilder(fSystemRepository, "system")
|
||||||
|
.AddPackages(B_PACKAGE_INSTALLATION_LOCATION_SYSTEM, "system")
|
||||||
|
.AddToSolver(fSolver, false);
|
||||||
|
fSystemRepository.SetPriority(-1);
|
||||||
|
|
||||||
|
bool installInHome = location == B_PACKAGE_INSTALLATION_LOCATION_HOME;
|
||||||
|
RepositoryBuilder(fCommonRepository, "common")
|
||||||
|
.AddPackages(B_PACKAGE_INSTALLATION_LOCATION_COMMON, "common")
|
||||||
|
.AddToSolver(fSolver, !installInHome);
|
||||||
|
|
||||||
|
if (!fInstalledRepositories.AddItem(&fSystemRepository)
|
||||||
|
|| !fInstalledRepositories.AddItem(&fCommonRepository)) {
|
||||||
|
DIE(B_NO_MEMORY, "failed to add installed repositories to list");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installInHome) {
|
||||||
|
fCommonRepository.SetPriority(-2);
|
||||||
|
RepositoryBuilder(fHomeRepository, "home")
|
||||||
|
.AddPackages(B_PACKAGE_INSTALLATION_LOCATION_HOME, "home")
|
||||||
|
.AddToSolver(fSolver, true);
|
||||||
|
|
||||||
|
if (!fInstalledRepositories.AddItem(&fHomeRepository))
|
||||||
|
DIE(B_NO_MEMORY, "failed to add home repository to list");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add other repositories
|
||||||
|
if (addOtherRepositories) {
|
||||||
|
BPackageRoster roster;
|
||||||
|
BStringList repositoryNames;
|
||||||
|
error = roster.GetRepositoryNames(repositoryNames);
|
||||||
|
if (error != B_OK)
|
||||||
|
WARN(error, "failed to get repository names");
|
||||||
|
|
||||||
|
int32 repositoryNameCount = repositoryNames.CountStrings();
|
||||||
|
for (int32 i = 0; i < repositoryNameCount; i++) {
|
||||||
|
Repository* repository = new(std::nothrow) Repository;
|
||||||
|
if (repository == NULL || !fOtherRepositories.AddItem(repository))
|
||||||
|
DIE(B_NO_MEMORY, "failed to create/add repository object");
|
||||||
|
|
||||||
|
const BString& name = repositoryNames.StringAt(i);
|
||||||
|
error = repository->Init(roster, fContext, name);
|
||||||
|
if (error != B_OK) {
|
||||||
|
WARN(error,
|
||||||
|
"failed to get config for repository \"%s\". Skipping.",
|
||||||
|
name.String());
|
||||||
|
fOtherRepositories.RemoveItem(repository, true);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RepositoryBuilder(*repository, repository->Config())
|
||||||
|
.AddToSolver(fSolver, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PackageManager::~PackageManager()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
PackageManager::Install(const char* const* packages, int packageCount)
|
||||||
|
{
|
||||||
|
// solve
|
||||||
|
BSolverPackageSpecifierList packagesToInstall;
|
||||||
|
for (int i = 0; i < packageCount; i++) {
|
||||||
|
if (!packagesToInstall.AppendSpecifier(packages[i]))
|
||||||
|
DIE(B_NO_MEMORY, "failed to add specified package");
|
||||||
|
}
|
||||||
|
|
||||||
|
const BSolverPackageSpecifier* unmatchedSpecifier;
|
||||||
|
status_t error = fSolver->Install(packagesToInstall, &unmatchedSpecifier);
|
||||||
|
if (error != B_OK) {
|
||||||
|
if (unmatchedSpecifier != NULL) {
|
||||||
|
DIE(error, "failed to find a match for \"%s\"",
|
||||||
|
unmatchedSpecifier->SelectString().String());
|
||||||
|
} else
|
||||||
|
DIE(error, "failed to compute packages to install");
|
||||||
|
}
|
||||||
|
|
||||||
|
_HandleProblems();
|
||||||
|
|
||||||
|
// install/uninstall packages
|
||||||
|
_AnalyzeResult();
|
||||||
|
_PrintResult();
|
||||||
|
_ApplyPackageChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
PackageManager::_HandleProblems()
|
||||||
|
{
|
||||||
|
while (fSolver->HasProblems()) {
|
||||||
|
printf("Encountered problems:\n");
|
||||||
|
|
||||||
|
int32 problemCount = fSolver->CountProblems();
|
||||||
|
for (int32 i = 0; i < problemCount; i++) {
|
||||||
|
// print problem and possible solutions
|
||||||
|
BSolverProblem* problem = fSolver->ProblemAt(i);
|
||||||
|
printf("problem %" B_PRId32 ": %s\n", i + 1,
|
||||||
|
problem->ToString().String());
|
||||||
|
|
||||||
|
int32 solutionCount = problem->CountSolutions();
|
||||||
|
for (int32 k = 0; k < solutionCount; k++) {
|
||||||
|
const BSolverProblemSolution* solution = problem->SolutionAt(k);
|
||||||
|
printf(" solution %" B_PRId32 ":\n", k + 1);
|
||||||
|
int32 elementCount = solution->CountElements();
|
||||||
|
for (int32 l = 0; l < elementCount; l++) {
|
||||||
|
const BSolverProblemSolutionElement* element
|
||||||
|
= solution->ElementAt(l);
|
||||||
|
printf(" - %s\n", element->ToString().String());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// let the user choose a solution
|
||||||
|
printf("Please select a solution, skip the problem for now or "
|
||||||
|
"quit.\n");
|
||||||
|
for (;;) {
|
||||||
|
if (solutionCount > 1)
|
||||||
|
printf("select [1...%" B_PRId32 "/s/q]: ", solutionCount);
|
||||||
|
else
|
||||||
|
printf("select [1/s/q]: ");
|
||||||
|
|
||||||
|
char buffer[32];
|
||||||
|
if (fgets(buffer, sizeof(buffer), stdin) == NULL
|
||||||
|
|| strcmp(buffer, "q\n") == 0) {
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(buffer, "s\n") == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
char* end;
|
||||||
|
long selected = strtol(buffer, &end, 0);
|
||||||
|
if (end == buffer || *end != '\n' || selected < 1
|
||||||
|
|| selected > solutionCount) {
|
||||||
|
printf("*** invalid input\n");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
status_t error = fSolver->SelectProblemSolution(problem,
|
||||||
|
problem->SolutionAt(selected - 1));
|
||||||
|
if (error != B_OK)
|
||||||
|
DIE(error, "failed to set solution");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status_t error = fSolver->SolveAgain();
|
||||||
|
if (error != B_OK)
|
||||||
|
DIE(error, "failed to compute packages to install");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
PackageManager::_AnalyzeResult()
|
||||||
|
{
|
||||||
|
BSolverResult result;
|
||||||
|
status_t error = fSolver->GetResult(result);
|
||||||
|
if (error != B_OK)
|
||||||
|
DIE(error, "failed to compute packages to install");
|
||||||
|
|
||||||
|
for (int32 i = 0; const BSolverResultElement* element = result.ElementAt(i);
|
||||||
|
i++) {
|
||||||
|
BSolverPackage* package = element->Package();
|
||||||
|
|
||||||
|
switch (element->Type()) {
|
||||||
|
case BSolverResultElement::B_TYPE_INSTALL:
|
||||||
|
if (!fInstalledRepositories.HasItem(package->Repository())) {
|
||||||
|
if (!fPackagesToActivate.AddItem(package))
|
||||||
|
DIE(B_NO_MEMORY, "failed to add package to activate");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BSolverResultElement::B_TYPE_UNINSTALL:
|
||||||
|
if (!fPackagesToDeactivate.AddItem(package))
|
||||||
|
DIE(B_NO_MEMORY, "failed to add package to deactivate");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fPackagesToActivate.IsEmpty() && fPackagesToDeactivate.IsEmpty()) {
|
||||||
|
printf("Nothing to do.\n");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
PackageManager::_PrintResult()
|
||||||
|
{
|
||||||
|
printf("The following changes will be made:\n");
|
||||||
|
for (int32 i = 0; BSolverPackage* package = fPackagesToActivate.ItemAt(i);
|
||||||
|
i++) {
|
||||||
|
printf(" install package %s from repository %s\n",
|
||||||
|
package->Info().CanonicalFileName().String(),
|
||||||
|
package->Repository()->Name().String());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32 i = 0; BSolverPackage* package = fPackagesToDeactivate.ItemAt(i);
|
||||||
|
i++) {
|
||||||
|
printf(" uninstall package %s\n", package->VersionedName().String());
|
||||||
|
}
|
||||||
|
// TODO: Print file/download sizes. Unfortunately our package infos don't
|
||||||
|
// contain the file size. Which is probably correct. The file size (and possibly
|
||||||
|
// other information) should, however, be provided by the repository cache in
|
||||||
|
// some way. Extend BPackageInfo? Create a BPackageFileInfo?
|
||||||
|
|
||||||
|
if (!fDecisionProvider.YesNoDecisionNeeded(BString(), "Continue?", "y", "n",
|
||||||
|
"y")) {
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
PackageManager::_ApplyPackageChanges()
|
||||||
|
{
|
||||||
|
// create an activation transaction
|
||||||
|
BDaemonClient daemonClient;
|
||||||
|
BActivationTransaction transaction;
|
||||||
|
BDirectory transactionDirectory;
|
||||||
|
status_t error = daemonClient.CreateTransaction(fLocation, transaction,
|
||||||
|
transactionDirectory);
|
||||||
|
if (error != B_OK)
|
||||||
|
DIE(error, "failed to create transaction");
|
||||||
|
|
||||||
|
// download the new packages and prepare the transaction
|
||||||
|
for (int32 i = 0; BSolverPackage* package = fPackagesToActivate.ItemAt(i);
|
||||||
|
i++) {
|
||||||
|
// get package URL and target entry
|
||||||
|
Repository* repository
|
||||||
|
= static_cast<Repository*>(package->Repository());
|
||||||
|
BString url = repository->Config().BaseURL();
|
||||||
|
BString fileName(package->Info().CanonicalFileName());
|
||||||
|
if (fileName.IsEmpty())
|
||||||
|
DIE(B_NO_MEMORY, "failed to allocate file name");
|
||||||
|
url << '/' << fileName;
|
||||||
|
|
||||||
|
BEntry entry;
|
||||||
|
error = entry.SetTo(&transactionDirectory, fileName);
|
||||||
|
if (error != B_OK)
|
||||||
|
DIE(error, "failed to create package entry");
|
||||||
|
|
||||||
|
// download the package
|
||||||
|
DownloadFileRequest downloadRequest(fContext, url, entry,
|
||||||
|
package->Info().Checksum());
|
||||||
|
error = downloadRequest.Process();
|
||||||
|
if (error != B_OK)
|
||||||
|
DIE(error, "failed to download package");
|
||||||
|
|
||||||
|
// add package to transaction
|
||||||
|
if (!transaction.AddPackageToActivate(
|
||||||
|
package->Info().CanonicalFileName())) {
|
||||||
|
DIE(B_NO_MEMORY,
|
||||||
|
"failed to add package to activate to transaction");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32 i = 0; BSolverPackage* package = fPackagesToDeactivate.ItemAt(i);
|
||||||
|
i++) {
|
||||||
|
// add package to transaction
|
||||||
|
if (!transaction.AddPackageToDeactivate(
|
||||||
|
package->Info().CanonicalFileName())) {
|
||||||
|
DIE(B_NO_MEMORY,
|
||||||
|
"failed to add package to deactivate to transaction");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// commit the transaction
|
||||||
|
BDaemonClient::BCommitTransactionResult transactionResult;
|
||||||
|
error = daemonClient.CommitTransaction(transaction, transactionResult);
|
||||||
|
if (error != B_OK) {
|
||||||
|
fprintf(stderr, "*** failed to commit transaction: %s\n",
|
||||||
|
transactionResult.FullErrorMessage().String());
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Installation done. Old activation state backed up in \"%s\"\n",
|
||||||
|
transactionResult.OldStateDirectory().String());
|
||||||
|
|
||||||
|
printf("Cleaning up ...\n");
|
||||||
|
BEntry transactionDirectoryEntry;
|
||||||
|
if ((error = transactionDirectory.GetEntry(&transactionDirectoryEntry))
|
||||||
|
!= B_OK
|
||||||
|
|| (error = transactionDirectoryEntry.Remove()) != B_OK) {
|
||||||
|
WARN(error, "failed to remove transaction directory");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013, Haiku, Inc. All Rights Reserved.
|
||||||
|
* Distributed under the terms of the MIT License.
|
||||||
|
*
|
||||||
|
* Authors:
|
||||||
|
* Ingo Weinhold <[email protected]>
|
||||||
|
*/
|
||||||
|
#ifndef PACKAGE_MANAGER_H
|
||||||
|
#define PACKAGE_MANAGER_H
|
||||||
|
|
||||||
|
|
||||||
|
#include <ObjectList.h>
|
||||||
|
#include <package/Context.h>
|
||||||
|
#include <package/PackageDefs.h>
|
||||||
|
#include <package/PackageRoster.h>
|
||||||
|
#include <package/RepositoryConfig.h>
|
||||||
|
#include <package/solver/Solver.h>
|
||||||
|
#include <package/solver/SolverRepository.h>
|
||||||
|
|
||||||
|
#include "DecisionProvider.h"
|
||||||
|
#include "JobStateListener.h"
|
||||||
|
|
||||||
|
|
||||||
|
using namespace BPackageKit;
|
||||||
|
|
||||||
|
|
||||||
|
class PackageManager {
|
||||||
|
public:
|
||||||
|
struct Repository;
|
||||||
|
typedef BObjectList<Repository> RepositoryList;
|
||||||
|
|
||||||
|
public:
|
||||||
|
PackageManager(
|
||||||
|
BPackageInstallationLocation location,
|
||||||
|
bool addInstalledRepositories,
|
||||||
|
bool addOtherRepositories);
|
||||||
|
~PackageManager();
|
||||||
|
|
||||||
|
BSolver* Solver() const
|
||||||
|
{ return fSolver; }
|
||||||
|
|
||||||
|
const BSolverRepository* SystemRepository() const
|
||||||
|
{ return &fSystemRepository; }
|
||||||
|
const BSolverRepository* CommonRepository() const
|
||||||
|
{ return &fCommonRepository; }
|
||||||
|
const BSolverRepository* HomeRepository() const
|
||||||
|
{ return &fHomeRepository; }
|
||||||
|
const BObjectList<BSolverRepository>& InstalledRepositories() const
|
||||||
|
{ return fInstalledRepositories; }
|
||||||
|
const RepositoryList& OtherRepositories() const
|
||||||
|
{ return fOtherRepositories; }
|
||||||
|
|
||||||
|
void Install(const char* const* packages,
|
||||||
|
int packageCount);
|
||||||
|
|
||||||
|
private:
|
||||||
|
typedef BObjectList<BSolverPackage> PackageList;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void _HandleProblems();
|
||||||
|
void _AnalyzeResult();
|
||||||
|
void _PrintResult();
|
||||||
|
void _ApplyPackageChanges();
|
||||||
|
|
||||||
|
private:
|
||||||
|
BPackageInstallationLocation fLocation;
|
||||||
|
BSolver* fSolver;
|
||||||
|
BSolverRepository fSystemRepository;
|
||||||
|
BSolverRepository fCommonRepository;
|
||||||
|
BSolverRepository fHomeRepository;
|
||||||
|
BObjectList<BSolverRepository> fInstalledRepositories;
|
||||||
|
RepositoryList fOtherRepositories;
|
||||||
|
DecisionProvider fDecisionProvider;
|
||||||
|
JobStateListener fJobStateListener;
|
||||||
|
BContext fContext;
|
||||||
|
PackageList fPackagesToActivate;
|
||||||
|
PackageList fPackagesToDeactivate;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
struct PackageManager::Repository : public BSolverRepository {
|
||||||
|
Repository();
|
||||||
|
|
||||||
|
status_t Init(BPackageRoster& roster, BContext& context,
|
||||||
|
const char* name);
|
||||||
|
|
||||||
|
const BRepositoryConfig& Config() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
BRepositoryConfig fConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif // PACKAGE_MANAGER_H
|
||||||
@@ -7,31 +7,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
#include <errno.h>
|
|
||||||
#include <getopt.h>
|
#include <getopt.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
#include <Directory.h>
|
|
||||||
#include <package/DownloadFileRequest.h>
|
|
||||||
#include <package/PackageRoster.h>
|
|
||||||
#include <package/RefreshRepositoryRequest.h>
|
|
||||||
#include <package/RepositoryConfig.h>
|
|
||||||
#include <package/solver/SolverPackageSpecifier.h>
|
|
||||||
#include <package/solver/SolverPackageSpecifierList.h>
|
|
||||||
#include <package/solver/SolverProblem.h>
|
|
||||||
#include <package/solver/SolverProblemSolution.h>
|
|
||||||
#include <package/solver/SolverResult.h>
|
|
||||||
|
|
||||||
#include <AutoDeleter.h>
|
|
||||||
#include <package/ActivationTransaction.h>
|
|
||||||
#include <package/DaemonClient.h>
|
|
||||||
|
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
#include "DecisionProvider.h"
|
|
||||||
#include "JobStateListener.h"
|
|
||||||
#include "pkgman.h"
|
#include "pkgman.h"
|
||||||
#include "RepositoryBuilder.h"
|
#include "PackageManager.h"
|
||||||
|
|
||||||
|
|
||||||
// TODO: internationalization!
|
// TODO: internationalization!
|
||||||
@@ -59,42 +41,6 @@ static const char* const kLongUsage =
|
|||||||
DEFINE_COMMAND(InstallCommand, "install", kShortUsage, kLongUsage)
|
DEFINE_COMMAND(InstallCommand, "install", kShortUsage, kLongUsage)
|
||||||
|
|
||||||
|
|
||||||
struct Repository : public BSolverRepository {
|
|
||||||
Repository()
|
|
||||||
:
|
|
||||||
BSolverRepository()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
status_t Init(BPackageRoster& roster, BContext& context, const char* name)
|
|
||||||
{
|
|
||||||
// get the repository config
|
|
||||||
status_t error = roster.GetRepositoryConfig(name, &fConfig);
|
|
||||||
if (error != B_OK)
|
|
||||||
return error;
|
|
||||||
|
|
||||||
// refresh
|
|
||||||
BRefreshRepositoryRequest refreshRequest(context, fConfig);
|
|
||||||
error = refreshRequest.Process();
|
|
||||||
if (error != B_OK) {
|
|
||||||
WARN(error, "refreshing repository \"%s\" failed", name);
|
|
||||||
return B_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
// re-get the config
|
|
||||||
return roster.GetRepositoryConfig(name, &fConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
const BRepositoryConfig& Config() const
|
|
||||||
{
|
|
||||||
return fConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
BRepositoryConfig fConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
int
|
int
|
||||||
InstallCommand::Execute(int argc, const char* const* argv)
|
InstallCommand::Execute(int argc, const char* const* argv)
|
||||||
{
|
{
|
||||||
@@ -134,289 +80,12 @@ InstallCommand::Execute(int argc, const char* const* argv)
|
|||||||
int packageCount = argc - optind;
|
int packageCount = argc - optind;
|
||||||
const char* const* packages = argv + optind;
|
const char* const* packages = argv + optind;
|
||||||
|
|
||||||
// create the solver
|
// perform the installation
|
||||||
BSolver* solver;
|
|
||||||
status_t error = BSolver::Create(solver);
|
|
||||||
if (error != B_OK)
|
|
||||||
DIE(error, "failed to create solver");
|
|
||||||
|
|
||||||
// add repositories
|
|
||||||
|
|
||||||
// We add only the repository of our actual installation location as the
|
|
||||||
// "installed" repository. The repositories for the more general
|
|
||||||
// installation locations are added as regular repositories, but with better
|
|
||||||
// priorities than the actual (remote) repositories. This prevents the solver
|
|
||||||
// from showing conflicts when a package in a more specific installation
|
|
||||||
// location overrides a package in a more general one. Instead any
|
|
||||||
// requirement that is already installed in a more general installation
|
|
||||||
// location will turn up as to be installed as well. But we can easily
|
|
||||||
// filter those out.
|
|
||||||
BSolverRepository systemRepository;
|
|
||||||
RepositoryBuilder(systemRepository, "system")
|
|
||||||
.AddPackages(B_PACKAGE_INSTALLATION_LOCATION_SYSTEM, "system")
|
|
||||||
.AddToSolver(solver, false);
|
|
||||||
systemRepository.SetPriority(-1);
|
|
||||||
|
|
||||||
BSolverRepository commonRepository;
|
|
||||||
RepositoryBuilder(commonRepository, "common")
|
|
||||||
.AddPackages(B_PACKAGE_INSTALLATION_LOCATION_COMMON, "common")
|
|
||||||
.AddToSolver(solver, !installInHome);
|
|
||||||
|
|
||||||
BObjectList<BSolverRepository> installedRepositories(10);
|
|
||||||
if (!installedRepositories.AddItem(&systemRepository)
|
|
||||||
|| !installedRepositories.AddItem(&commonRepository)) {
|
|
||||||
DIE(B_NO_MEMORY, "failed to add installed repositories to list");
|
|
||||||
}
|
|
||||||
|
|
||||||
BSolverRepository homeRepository;
|
|
||||||
if (installInHome) {
|
|
||||||
commonRepository.SetPriority(-2);
|
|
||||||
RepositoryBuilder(homeRepository, "home")
|
|
||||||
.AddPackages(B_PACKAGE_INSTALLATION_LOCATION_HOME, "home")
|
|
||||||
.AddToSolver(solver, true);
|
|
||||||
|
|
||||||
if (!installedRepositories.AddItem(&homeRepository))
|
|
||||||
DIE(B_NO_MEMORY, "failed to add home repository to list");
|
|
||||||
}
|
|
||||||
|
|
||||||
// other repositories
|
|
||||||
DecisionProvider decisionProvider;
|
|
||||||
JobStateListener listener;
|
|
||||||
BContext context(decisionProvider, listener);
|
|
||||||
|
|
||||||
BObjectList<Repository> otherRepositories(10, true);
|
|
||||||
BPackageRoster roster;
|
|
||||||
BStringList repositoryNames;
|
|
||||||
error = roster.GetRepositoryNames(repositoryNames);
|
|
||||||
if (error != B_OK)
|
|
||||||
WARN(error, "failed to get repository names");
|
|
||||||
|
|
||||||
int32 repositoryNameCount = repositoryNames.CountStrings();
|
|
||||||
for (int32 i = 0; i < repositoryNameCount; i++) {
|
|
||||||
Repository* repository = new(std::nothrow) Repository;
|
|
||||||
if (repository == NULL || !otherRepositories.AddItem(repository))
|
|
||||||
DIE(B_NO_MEMORY, "failed to create/add repository object");
|
|
||||||
|
|
||||||
const BString& name = repositoryNames.StringAt(i);
|
|
||||||
error = repository->Init(roster, context, name);
|
|
||||||
if (error != B_OK) {
|
|
||||||
WARN(error, "failed to get config for repository \"%s\". Skipping.",
|
|
||||||
name.String());
|
|
||||||
otherRepositories.RemoveItem(repository, true);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
RepositoryBuilder(*repository, repository->Config())
|
|
||||||
.AddToSolver(solver, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// solve
|
|
||||||
BSolverPackageSpecifierList packagesToInstall;
|
|
||||||
for (int i = 0; i < packageCount; i++) {
|
|
||||||
if (!packagesToInstall.AppendSpecifier(packages[i]))
|
|
||||||
DIE(B_NO_MEMORY, "failed to add specified package");
|
|
||||||
}
|
|
||||||
|
|
||||||
const BSolverPackageSpecifier* unmatchedSpecifier;
|
|
||||||
error = solver->Install(packagesToInstall, &unmatchedSpecifier);
|
|
||||||
if (error != B_OK) {
|
|
||||||
if (unmatchedSpecifier != NULL) {
|
|
||||||
DIE(error, "failed to find a match for \"%s\"",
|
|
||||||
unmatchedSpecifier->SelectString().String());
|
|
||||||
} else
|
|
||||||
DIE(error, "failed to compute packages to install");
|
|
||||||
}
|
|
||||||
|
|
||||||
// deal with problems
|
|
||||||
while (solver->HasProblems()) {
|
|
||||||
printf("Encountered problems:\n");
|
|
||||||
|
|
||||||
int32 problemCount = solver->CountProblems();
|
|
||||||
for (int32 i = 0; i < problemCount; i++) {
|
|
||||||
// print problem and possible solutions
|
|
||||||
BSolverProblem* problem = solver->ProblemAt(i);
|
|
||||||
printf("problem %" B_PRId32 ": %s\n", i + 1,
|
|
||||||
problem->ToString().String());
|
|
||||||
|
|
||||||
int32 solutionCount = problem->CountSolutions();
|
|
||||||
for (int32 k = 0; k < solutionCount; k++) {
|
|
||||||
const BSolverProblemSolution* solution = problem->SolutionAt(k);
|
|
||||||
printf(" solution %" B_PRId32 ":\n", k + 1);
|
|
||||||
int32 elementCount = solution->CountElements();
|
|
||||||
for (int32 l = 0; l < elementCount; l++) {
|
|
||||||
const BSolverProblemSolutionElement* element
|
|
||||||
= solution->ElementAt(l);
|
|
||||||
printf(" - %s\n", element->ToString().String());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// let the user choose a solution
|
|
||||||
printf("Please select a solution, skip the problem for now or "
|
|
||||||
"quit.\n");
|
|
||||||
for (;;) {
|
|
||||||
if (solutionCount > 1)
|
|
||||||
printf("select [1...%" B_PRId32 "/s/q]: ", solutionCount);
|
|
||||||
else
|
|
||||||
printf("select [1/s/q]: ");
|
|
||||||
|
|
||||||
char buffer[32];
|
|
||||||
if (fgets(buffer, sizeof(buffer), stdin) == NULL
|
|
||||||
|| strcmp(buffer, "q\n") == 0) {
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strcmp(buffer, "s\n") == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
char* end;
|
|
||||||
long selected = strtol(buffer, &end, 0);
|
|
||||||
if (end == buffer || *end != '\n' || selected < 1
|
|
||||||
|| selected > solutionCount) {
|
|
||||||
printf("*** invalid input\n");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
error = solver->SelectProblemSolution(problem,
|
|
||||||
problem->SolutionAt(selected - 1));
|
|
||||||
if (error != B_OK)
|
|
||||||
DIE(error, "failed to set solution");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
error = solver->SolveAgain();
|
|
||||||
if (error != B_OK)
|
|
||||||
DIE(error, "failed to compute packages to install");
|
|
||||||
}
|
|
||||||
|
|
||||||
// print result
|
|
||||||
BSolverResult result;
|
|
||||||
error = solver->GetResult(result);
|
|
||||||
if (error != B_OK)
|
|
||||||
DIE(error, "failed to compute packages to install");
|
|
||||||
|
|
||||||
BObjectList<BSolverPackage> packagesToActivate;
|
|
||||||
BObjectList<BSolverPackage> packagesToDeactivate;
|
|
||||||
|
|
||||||
for (int32 i = 0; const BSolverResultElement* element = result.ElementAt(i);
|
|
||||||
i++) {
|
|
||||||
BSolverPackage* package = element->Package();
|
|
||||||
|
|
||||||
switch (element->Type()) {
|
|
||||||
case BSolverResultElement::B_TYPE_INSTALL:
|
|
||||||
if (!installedRepositories.HasItem(package->Repository())) {
|
|
||||||
if (!packagesToActivate.AddItem(package))
|
|
||||||
DIE(B_NO_MEMORY, "failed to add package to activate");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BSolverResultElement::B_TYPE_UNINSTALL:
|
|
||||||
if (!packagesToDeactivate.AddItem(package))
|
|
||||||
DIE(B_NO_MEMORY, "failed to add package to deactivate");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (packagesToActivate.IsEmpty() && packagesToDeactivate.IsEmpty()) {
|
|
||||||
printf("Nothing to do.\n");
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("The following changes will be made:\n");
|
|
||||||
for (int32 i = 0; BSolverPackage* package = packagesToActivate.ItemAt(i);
|
|
||||||
i++) {
|
|
||||||
printf(" install package %s from repository %s\n",
|
|
||||||
package->Info().CanonicalFileName().String(),
|
|
||||||
package->Repository()->Name().String());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int32 i = 0; BSolverPackage* package = packagesToDeactivate.ItemAt(i);
|
|
||||||
i++) {
|
|
||||||
printf(" uninstall package %s\n", package->VersionedName().String());
|
|
||||||
}
|
|
||||||
// TODO: Print file/download sizes. Unfortunately our package infos don't
|
|
||||||
// contain the file size. Which is probably correct. The file size (and possibly
|
|
||||||
// other information) should, however, be provided by the repository cache in
|
|
||||||
// some way. Extend BPackageInfo? Create a BPackageFileInfo?
|
|
||||||
|
|
||||||
if (!decisionProvider.YesNoDecisionNeeded(BString(), "Continue?", "y", "n",
|
|
||||||
"y")) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create an activation transaction
|
|
||||||
BDaemonClient daemonClient;
|
|
||||||
BPackageInstallationLocation location = installInHome
|
BPackageInstallationLocation location = installInHome
|
||||||
? B_PACKAGE_INSTALLATION_LOCATION_HOME
|
? B_PACKAGE_INSTALLATION_LOCATION_HOME
|
||||||
: B_PACKAGE_INSTALLATION_LOCATION_COMMON;
|
: B_PACKAGE_INSTALLATION_LOCATION_COMMON;
|
||||||
BActivationTransaction transaction;
|
PackageManager packageManager(location, true, true);
|
||||||
BDirectory transactionDirectory;
|
packageManager.Install(packages, packageCount);
|
||||||
error = daemonClient.CreateTransaction(location, transaction,
|
|
||||||
transactionDirectory);
|
|
||||||
if (error != B_OK)
|
|
||||||
DIE(error, "failed to create transaction");
|
|
||||||
|
|
||||||
// download the new packages and prepare the transaction
|
|
||||||
for (int32 i = 0; BSolverPackage* package = packagesToActivate.ItemAt(i);
|
|
||||||
i++) {
|
|
||||||
// get package URL and target entry
|
|
||||||
Repository* repository
|
|
||||||
= static_cast<Repository*>(package->Repository());
|
|
||||||
BString url = repository->Config().BaseURL();
|
|
||||||
BString fileName(package->Info().CanonicalFileName());
|
|
||||||
if (fileName.IsEmpty())
|
|
||||||
DIE(B_NO_MEMORY, "failed to allocate file name");
|
|
||||||
url << '/' << fileName;
|
|
||||||
|
|
||||||
BEntry entry;
|
|
||||||
error = entry.SetTo(&transactionDirectory, fileName);
|
|
||||||
if (error != B_OK)
|
|
||||||
DIE(error, "failed to create package entry");
|
|
||||||
|
|
||||||
// download the package
|
|
||||||
DownloadFileRequest downloadRequest(context, url, entry,
|
|
||||||
package->Info().Checksum());
|
|
||||||
error = downloadRequest.Process();
|
|
||||||
if (error != B_OK)
|
|
||||||
DIE(error, "failed to download package");
|
|
||||||
|
|
||||||
// add package to transaction
|
|
||||||
if (!transaction.AddPackageToActivate(
|
|
||||||
package->Info().CanonicalFileName())) {
|
|
||||||
DIE(B_NO_MEMORY,
|
|
||||||
"failed to add package to activate to transaction");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int32 i = 0; BSolverPackage* package = packagesToDeactivate.ItemAt(i);
|
|
||||||
i++) {
|
|
||||||
// add package to transaction
|
|
||||||
if (!transaction.AddPackageToDeactivate(
|
|
||||||
package->Info().CanonicalFileName())) {
|
|
||||||
DIE(B_NO_MEMORY,
|
|
||||||
"failed to add package to deactivate to transaction");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// commit the transaction
|
|
||||||
BDaemonClient::BCommitTransactionResult transactionResult;
|
|
||||||
error = daemonClient.CommitTransaction(transaction, transactionResult);
|
|
||||||
if (error != B_OK) {
|
|
||||||
fprintf(stderr, "*** failed to commit transaction: %s\n",
|
|
||||||
transactionResult.FullErrorMessage().String());
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("Installation done. Old activation state backed up in \"%s\"\n",
|
|
||||||
transactionResult.OldStateDirectory().String());
|
|
||||||
|
|
||||||
printf("Cleaning up ...\n");
|
|
||||||
BEntry transactionDirectoryEntry;
|
|
||||||
if ((error = transactionDirectory.GetEntry(&transactionDirectoryEntry))
|
|
||||||
!= B_OK
|
|
||||||
|| (error = transactionDirectoryEntry.Remove()) != B_OK) {
|
|
||||||
WARN(error, "failed to remove transaction directory");
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,13 @@
|
|||||||
#include <sys/ioctl.h>
|
#include <sys/ioctl.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <package/PackageRoster.h>
|
#include <algorithm>
|
||||||
#include <package/RepositoryConfig.h>
|
|
||||||
|
|
||||||
#include <AutoDeleter.h>
|
#include <package/solver/SolverPackage.h>
|
||||||
|
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
|
#include "PackageManager.h"
|
||||||
#include "pkgman.h"
|
#include "pkgman.h"
|
||||||
#include "RepositoryBuilder.h"
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: internationalization!
|
// TODO: internationalization!
|
||||||
@@ -32,9 +31,6 @@
|
|||||||
using namespace BPackageKit;
|
using namespace BPackageKit;
|
||||||
|
|
||||||
|
|
||||||
typedef std::map<BSolverPackage*, BString> PackagePathMap;
|
|
||||||
|
|
||||||
|
|
||||||
static const char* const kShortUsage =
|
static const char* const kShortUsage =
|
||||||
" %command% <search-string>\n"
|
" %command% <search-string>\n"
|
||||||
" Searches for packages matching <search-string>.\n";
|
" Searches for packages matching <search-string>.\n";
|
||||||
@@ -113,64 +109,13 @@ SearchCommand::Execute(int argc, const char* const* argv)
|
|||||||
const char* searchString = argv[optind++];
|
const char* searchString = argv[optind++];
|
||||||
|
|
||||||
// create the solver
|
// create the solver
|
||||||
BSolver* solver;
|
PackageManager packageManager(B_PACKAGE_INSTALLATION_LOCATION_COMMON,
|
||||||
status_t error = BSolver::Create(solver);
|
!uninstalledOnly, !installedOnly);
|
||||||
if (error != B_OK)
|
// TODO: Use B_PACKAGE_INSTALLATION_LOCATION_HOME once we actually mount it.
|
||||||
DIE(error, "failed to create solver");
|
|
||||||
|
|
||||||
// add repositories
|
|
||||||
|
|
||||||
// installed
|
|
||||||
BSolverRepository systemRepository;
|
|
||||||
BSolverRepository commonRepository;
|
|
||||||
BSolverRepository homeRepository;
|
|
||||||
if (!uninstalledOnly) {
|
|
||||||
RepositoryBuilder(systemRepository, "system")
|
|
||||||
.AddPackages(B_PACKAGE_INSTALLATION_LOCATION_SYSTEM, "system")
|
|
||||||
.AddToSolver(solver, false);
|
|
||||||
RepositoryBuilder(commonRepository, "common")
|
|
||||||
.AddPackages(B_PACKAGE_INSTALLATION_LOCATION_COMMON, "common")
|
|
||||||
.AddToSolver(solver, false);
|
|
||||||
// RepositoryBuilder(homeRepository, "home")
|
|
||||||
// .AddPackages(B_PACKAGE_INSTALLATION_LOCATION_HOME, "home")
|
|
||||||
// .AddToSolver(solver, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// not installed
|
|
||||||
BObjectList<BSolverRepository> uninstalledRepositories(10, true);
|
|
||||||
|
|
||||||
if (!installedOnly) {
|
|
||||||
BPackageRoster roster;
|
|
||||||
BStringList repositoryNames;
|
|
||||||
error = roster.GetRepositoryNames(repositoryNames);
|
|
||||||
if (error != B_OK)
|
|
||||||
WARN(error, "failed to get repository names");
|
|
||||||
|
|
||||||
int32 repositoryNameCount = repositoryNames.CountStrings();
|
|
||||||
for (int32 i = 0; i < repositoryNameCount; i++) {
|
|
||||||
const BString& name = repositoryNames.StringAt(i);
|
|
||||||
BRepositoryConfig config;
|
|
||||||
error = roster.GetRepositoryConfig(name, &config);
|
|
||||||
if (error != B_OK) {
|
|
||||||
WARN(error, "failed to get config for repository \"%s\". "
|
|
||||||
"Skipping.", name.String());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
BSolverRepository* repository = new(std::nothrow) BSolverRepository;
|
|
||||||
if (repository == NULL
|
|
||||||
|| !uninstalledRepositories.AddItem(repository)) {
|
|
||||||
DIE(B_NO_MEMORY, "out of memory");
|
|
||||||
}
|
|
||||||
|
|
||||||
RepositoryBuilder(*repository, config)
|
|
||||||
.AddToSolver(solver, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// search
|
// search
|
||||||
BObjectList<BSolverPackage> packages;
|
BObjectList<BSolverPackage> packages;
|
||||||
error = solver->FindPackages(searchString,
|
status_t error = packageManager.Solver()->FindPackages(searchString,
|
||||||
BSolver::B_FIND_CASE_INSENSITIVE | BSolver::B_FIND_IN_NAME
|
BSolver::B_FIND_CASE_INSENSITIVE | BSolver::B_FIND_IN_NAME
|
||||||
| BSolver::B_FIND_IN_SUMMARY | BSolver::B_FIND_IN_DESCRIPTION
|
| BSolver::B_FIND_IN_SUMMARY | BSolver::B_FIND_IN_DESCRIPTION
|
||||||
| BSolver::B_FIND_IN_PROVIDES,
|
| BSolver::B_FIND_IN_PROVIDES,
|
||||||
@@ -230,11 +175,11 @@ SearchCommand::Execute(int argc, const char* const* argv)
|
|||||||
BSolverPackage* package = packages.ItemAt(i);
|
BSolverPackage* package = packages.ItemAt(i);
|
||||||
|
|
||||||
const char* installed = "";
|
const char* installed = "";
|
||||||
if (package->Repository() == &systemRepository)
|
if (package->Repository() == packageManager.SystemRepository())
|
||||||
installed = "system";
|
installed = "system";
|
||||||
else if (package->Repository() == &commonRepository)
|
else if (package->Repository() == packageManager.CommonRepository())
|
||||||
installed = "common";
|
installed = "common";
|
||||||
else if (package->Repository() == &homeRepository)
|
else if (package->Repository() == packageManager.HomeRepository())
|
||||||
installed = "home";
|
installed = "home";
|
||||||
|
|
||||||
printf("%-*s %-*s %-*.*s\n",
|
printf("%-*s %-*s %-*.*s\n",
|
||||||
|
|||||||
Reference in New Issue
Block a user