diff options
author | Julian Andres Klode <julian.klode@canonical.com> | 2018-04-13 12:32:35 +0200 |
---|---|---|
committer | Julian Andres Klode <julian.klode@canonical.com> | 2018-04-15 21:11:42 +0200 |
commit | e9796b9c21ee7d8e8f5d6e2a24db43fc4368b557 (patch) | |
tree | 110d8fd225b790e491793e3619f470548b3e9c5a /apt-private | |
parent | 03cc48f6c6591d762c27f9b5c8627b267a7158e2 (diff) |
Introduce experimental new hooks for command-line tools
This allows third-party package managers like snap or flatpak
to hook in and suggest alternatives if packages could not be
found, for example.
This is still highly experimental and the protocol might change
in future versions.
Diffstat (limited to 'apt-private')
-rw-r--r-- | apt-private/private-install.cc | 29 | ||||
-rw-r--r-- | apt-private/private-install.h | 2 | ||||
-rw-r--r-- | apt-private/private-json-hooks.cc | 425 | ||||
-rw-r--r-- | apt-private/private-json-hooks.h | 14 | ||||
-rw-r--r-- | apt-private/private-search.cc | 12 |
5 files changed, 475 insertions, 7 deletions
diff --git a/apt-private/private-install.cc b/apt-private/private-install.cc index b24b96351..e1beb21c6 100644 --- a/apt-private/private-install.cc +++ b/apt-private/private-install.cc @@ -35,6 +35,7 @@ #include <apt-private/private-cacheset.h> #include <apt-private/private-download.h> #include <apt-private/private-install.h> +#include <apt-private/private-json-hooks.h> #include <apt-private/private-output.h> #include <apti18n.h> @@ -569,10 +570,11 @@ bool DoCacheManipulationFromCommandLine(CommandLine &CmdL, CacheFile &Cache, int bool DoCacheManipulationFromCommandLine(CommandLine &CmdL, std::vector<std::string> &VolatileCmdL, CacheFile &Cache, int UpgradeMode) { std::map<unsigned short, APT::VersionSet> verset; - return DoCacheManipulationFromCommandLine(CmdL, VolatileCmdL, Cache, verset, UpgradeMode); + std::set<std::string> UnknownPackages; + return DoCacheManipulationFromCommandLine(CmdL, VolatileCmdL, Cache, verset, UpgradeMode, UnknownPackages); } bool DoCacheManipulationFromCommandLine(CommandLine &CmdL, std::vector<std::string> &VolatileCmdL, CacheFile &Cache, - std::map<unsigned short, APT::VersionSet> &verset, int UpgradeMode) + std::map<unsigned short, APT::VersionSet> &verset, int UpgradeMode, std::set<std::string> &UnknownPackages) { // Enter the special broken fixing mode if the user specified arguments bool BrokenFix = false; @@ -621,6 +623,8 @@ bool DoCacheManipulationFromCommandLine(CommandLine &CmdL, std::vector<std::stri APT::VersionContainerInterface::FromPackage(&(verset[MOD_INSTALL]), Cache, P, APT::CacheSetHelper::CANDIDATE, helper); } + UnknownPackages = helper.notFound; + if (_error->PendingError() == true) { helper.showVirtualPackageErrors(Cache); @@ -726,8 +730,13 @@ bool DoInstall(CommandLine &CmdL) return false; std::map<unsigned short, APT::VersionSet> verset; - if(!DoCacheManipulationFromCommandLine(CmdL, VolatileCmdL, Cache, verset, 0)) + std::set<std::string> UnknownPackages; + + if (!DoCacheManipulationFromCommandLine(CmdL, VolatileCmdL, Cache, verset, 0, UnknownPackages)) + { + RunJsonHook("AptCli::Hooks::Install", "org.debian.apt.hooks.install.fail", CmdL.FileList, Cache, UnknownPackages); return false; + } /* Print out a list of packages that are going to be installed extra to what the user asked */ @@ -828,12 +837,22 @@ bool DoInstall(CommandLine &CmdL) always_true, string_ident, verbose_show_candidate); } + RunJsonHook("AptCli::Hooks::Install", "org.debian.apt.hooks.install.pre-prompt", CmdL.FileList, Cache); + + bool result; // See if we need to prompt // FIXME: check if really the packages in the set are going to be installed if (Cache->InstCount() == verset[MOD_INSTALL].size() && Cache->DelCount() == 0) - return InstallPackages(Cache,false,false); + result = InstallPackages(Cache, false, false); + else + result = InstallPackages(Cache, false); + + if (result) + result = RunJsonHook("AptCli::Hooks::Install", "org.debian.apt.hooks.install.post", CmdL.FileList, Cache); + else + /* not a result */ RunJsonHook("AptCli::Hooks::Install", "org.debian.apt.hooks.install.fail", CmdL.FileList, Cache); - return InstallPackages(Cache,false); + return result; } /*}}}*/ diff --git a/apt-private/private-install.h b/apt-private/private-install.h index c8b065331..2d27756c9 100644 --- a/apt-private/private-install.h +++ b/apt-private/private-install.h @@ -18,7 +18,7 @@ class pkgProblemResolver; APT_PUBLIC bool DoInstall(CommandLine &Cmd); bool DoCacheManipulationFromCommandLine(CommandLine &CmdL, std::vector<std::string> &VolatileCmdL, CacheFile &Cache, - std::map<unsigned short, APT::VersionSet> &verset, int UpgradeMode); + std::map<unsigned short, APT::VersionSet> &verset, int UpgradeMode, std::set<std::string> &UnknownPackages); bool DoCacheManipulationFromCommandLine(CommandLine &CmdL, std::vector<std::string> &VolatileCmdL, CacheFile &Cache, int UpgradeMode); bool DoCacheManipulationFromCommandLine(CommandLine &CmdL, CacheFile &Cache, int UpgradeMode); diff --git a/apt-private/private-json-hooks.cc b/apt-private/private-json-hooks.cc new file mode 100644 index 000000000..07c89ca23 --- /dev/null +++ b/apt-private/private-json-hooks.cc @@ -0,0 +1,425 @@ +/* + * private-json-hooks.cc - 2nd generation, JSON-RPC, hooks for APT + * + * Copyright (c) 2018 Canonical Ltd + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <apt-pkg/debsystem.h> +#include <apt-pkg/macros.h> +#include <apt-private/private-json-hooks.h> + +#include <ostream> +#include <sstream> +#include <stack> + +#include <signal.h> +#include <sys/socket.h> +#include <sys/types.h> + +/** + * @brief Simple JSON writer + * + * This performs no error checking, or string escaping, be careful. + */ +class APT_HIDDEN JsonWriter +{ + std::ostream &os; + std::locale old_locale; + + enum write_state + { + empty, + in_array_first_element, + in_array, + in_object_first_key, + in_object_key, + in_object_val + } state = empty; + + std::stack<write_state> old_states; + + void maybeComma() + { + switch (state) + { + case empty: + break; + case in_object_val: + state = in_object_key; + break; + case in_object_key: + state = in_object_val; + os << ','; + break; + case in_array: + os << ','; + break; + case in_array_first_element: + state = in_array; + break; + case in_object_first_key: + state = in_object_val; + break; + default: + abort(); + } + } + + void pushState(write_state state) + { + old_states.push(this->state); + this->state = state; + } + + void popState() + { + this->state = old_states.top(); + } + + public: + JsonWriter(std::ostream &os) : os(os) { old_locale = os.imbue(std::locale::classic()); } + ~JsonWriter() { os.imbue(old_locale); } + JsonWriter &beginArray() + { + maybeComma(); + pushState(in_array_first_element); + os << '['; + return *this; + } + JsonWriter &endArray() + { + popState(); + os << ']'; + return *this; + } + JsonWriter &beginObject() + { + maybeComma(); + pushState(in_object_first_key); + os << '{'; + return *this; + } + JsonWriter &endObject() + { + popState(); + os << '}'; + return *this; + } + JsonWriter &name(std::string const &name) + { + maybeComma(); + os << '"' << name << '"' << ':'; + return *this; + } + JsonWriter &value(std::string const &value) + { + maybeComma(); + os << '"' << value << '"'; + return *this; + } + JsonWriter &value(const char *value) + { + maybeComma(); + os << '"' << value << '"'; + return *this; + } + JsonWriter &value(int value) + { + maybeComma(); + os << value; + return *this; + } + JsonWriter &value(long value) + { + maybeComma(); + os << value; + return *this; + } + JsonWriter &value(long long value) + { + maybeComma(); + os << value; + return *this; + } + JsonWriter &value(unsigned long long value) + { + maybeComma(); + os << value; + return *this; + } + JsonWriter &value(unsigned long value) + { + maybeComma(); + os << value; + return *this; + } + JsonWriter &value(unsigned int value) + { + maybeComma(); + os << value; + return *this; + } + JsonWriter &value(bool value) + { + maybeComma(); + os << (value ? "true" : "false"); + return *this; + } + JsonWriter &value(double value) + { + maybeComma(); + os << value; + return *this; + } +}; + +/** + * @brief Wrtie a VerIterator to a JsonWriter + */ +static void verIterToJson(JsonWriter &writer, CacheFile &Cache, pkgCache::VerIterator const &Ver) +{ + writer.beginObject(); + writer.name("id").value(Ver->ID); + writer.name("version").value(Ver.VerStr()); + writer.name("architecture").value(Ver.Arch()); + writer.name("pin").value(Cache->GetPolicy().GetPriority(Ver)); + writer.endObject(); +} + +/** + * @brief Copy of debSystem::DpkgChrootDirectory() + * @todo Remove + */ +static void DpkgChrootDirectory() +{ + std::string const chrootDir = _config->FindDir("DPkg::Chroot-Directory"); + if (chrootDir == "/") + return; + std::cerr << "Chrooting into " << chrootDir << std::endl; + if (chroot(chrootDir.c_str()) != 0) + _exit(100); + if (chdir("/") != 0) + _exit(100); +} + +/** + * @brief Send a notification to the hook's stream + */ +static void NotifyHook(std::ostream &os, std::string const &method, const char **FileList, CacheFile &Cache, std::set<std::string> const &UnknownPackages) +{ + SortedPackageUniverse Universe(Cache); + JsonWriter jsonWriter{os}; + + jsonWriter.beginObject(); + + jsonWriter.name("jsonrpc").value("2.0"); + jsonWriter.name("method").value(method); + + /* Build params */ + jsonWriter.name("params").beginObject(); + jsonWriter.name("command").value(FileList[0]); + jsonWriter.name("search-terms").beginArray(); + for (int i = 1; FileList[i] != NULL; i++) + jsonWriter.value(FileList[i]); + jsonWriter.endArray(); + jsonWriter.name("unknown-packages").beginArray(); + for (auto const &PkgName : UnknownPackages) + jsonWriter.value(PkgName); + jsonWriter.endArray(); + + jsonWriter.name("packages").beginArray(); + for (auto const &Pkg : Universe) + { + switch (Cache[Pkg].Mode) + { + case pkgDepCache::ModeInstall: + case pkgDepCache::ModeDelete: + break; + default: + continue; + } + + jsonWriter.beginObject(); + + jsonWriter.name("id").value(Pkg->ID); + jsonWriter.name("name").value(Pkg.Name()); + jsonWriter.name("architecture").value(Pkg.Arch()); + + switch (Cache[Pkg].Mode) + { + case pkgDepCache::ModeInstall: + jsonWriter.name("mode").value("install"); + break; + case pkgDepCache::ModeDelete: + jsonWriter.name("mode").value(Cache[Pkg].Purge() ? "purge" : "deinstall"); + break; + default: + continue; + } + jsonWriter.name("automatic").value(bool(Cache[Pkg].Flags & pkgCache::Flag::Auto)); + + jsonWriter.name("versions").beginObject(); + + if (Cache[Pkg].CandidateVer != nullptr) + verIterToJson(jsonWriter.name("candidate"), Cache, Cache[Pkg].CandidateVerIter(Cache)); + if (Cache[Pkg].InstallVer != nullptr) + verIterToJson(jsonWriter.name("install"), Cache, Cache[Pkg].InstVerIter(Cache)); + if (Pkg->CurrentVer != 0) + verIterToJson(jsonWriter.name("current"), Cache, Pkg.CurrentVer()); + + jsonWriter.endObject(); + + jsonWriter.endObject(); + } + + jsonWriter.endArray(); // packages + jsonWriter.endObject(); // params + jsonWriter.endObject(); // main +} + +/// @brief Build the hello handshake message for 0.1 protocol +static std::string BuildHelloMessage() +{ + std::stringstream Hello; + JsonWriter(Hello).beginObject().name("jsonrpc").value("2.0").name("method").value("org.debian.apt.hooks.hello").name("id").value(0).name("params").beginObject().name("versions").beginArray().value("0.1").endArray().endObject().endObject(); + + return Hello.str(); +} + +/// @brief Build the bye notification for 0.1 protocol +static std::string BuildByeMessage() +{ + std::stringstream Bye; + JsonWriter(Bye).beginObject().name("jsonrpc").value("2.0").name("method").value("org.debian.apt.hooks.bye").name("params").beginObject().endObject().endObject(); + + return Bye.str(); +} + +/// @brief Run the Json hook processes in the given option. +bool RunJsonHook(std::string const &option, std::string const &method, const char **FileList, CacheFile &Cache, std::set<std::string> const &UnknownPackages) +{ + std::stringstream ss; + NotifyHook(ss, method, FileList, Cache, UnknownPackages); + std::string TheData = ss.str(); + std::string HelloData = BuildHelloMessage(); + std::string ByeData = BuildByeMessage(); + + bool result = true; + + Configuration::Item const *Opts = _config->Tree(option.c_str()); + if (Opts == 0 || Opts->Child == 0) + return true; + Opts = Opts->Child; + + sighandler_t old_sigpipe = signal(SIGPIPE, SIG_IGN); + sighandler_t old_sigint = signal(SIGINT, SIG_IGN); + sighandler_t old_sigquit = signal(SIGQUIT, SIG_IGN); + + unsigned int Count = 1; + for (; Opts != 0; Opts = Opts->Next, Count++) + { + if (Opts->Value.empty() == true) + continue; + + if (_config->FindB("Debug::RunScripts", false) == true) + std::clog << "Running external script with list of all .deb file: '" + << Opts->Value << "'" << std::endl; + + // Create the pipes + std::set<int> KeepFDs; + MergeKeepFdsFromConfiguration(KeepFDs); + int Pipes[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, Pipes) != 0) + { + result = _error->Errno("pipe", "Failed to create IPC pipe to subprocess"); + break; + } + + int InfoFD = 3; + + if (InfoFD != Pipes[0]) + SetCloseExec(Pipes[0], true); + else + KeepFDs.insert(Pipes[0]); + + SetCloseExec(Pipes[1], true); + + // Purified Fork for running the script + pid_t Process = ExecFork(KeepFDs); + if (Process == 0) + { + // Setup the FDs + dup2(Pipes[0], InfoFD); + SetCloseExec(STDOUT_FILENO, false); + SetCloseExec(STDIN_FILENO, false); + SetCloseExec(STDERR_FILENO, false); + + string hookfd; + strprintf(hookfd, "%d", InfoFD); + setenv("APT_HOOK_SOCKET", hookfd.c_str(), 1); + + DpkgChrootDirectory(); + const char *Args[4]; + Args[0] = "/bin/sh"; + Args[1] = "-c"; + Args[2] = Opts->Value.c_str(); + Args[3] = 0; + execv(Args[0], (char **)Args); + _exit(100); + } + close(Pipes[0]); + FILE *F = fdopen(Pipes[1], "w+"); + if (F == 0) + { + result = _error->Errno("fdopen", "Failed to open new FD"); + break; + } + + fwrite(HelloData.data(), HelloData.size(), 1, F); + fwrite("\n\n", 2, 1, F); + fflush(F); + + char *line = nullptr; + size_t linesize = 0; + ssize_t size = getline(&line, &linesize, F); + + if (size < 0) + { + _error->Error("Could not read response to hello message from hook %s: %s", Opts->Value.c_str(), strerror(errno)); + } + else if (strstr(line, "error") != nullptr) + { + _error->Error("Hook %s reported an error during hello: %s", Opts->Value.c_str(), line); + } + + size = getline(&line, &linesize, F); + if (size < 0) + { + _error->Error("Could not read message separator line after handshake from %s: %s", Opts->Value.c_str(), strerror(errno)); + } + else if (size == 0 || line[0] != '\n') + { + _error->Error("Expected empty line after handshake from %s, received %s", Opts->Value.c_str(), line); + } + + fwrite(TheData.data(), TheData.size(), 1, F); + fwrite("\n\n", 2, 1, F); + + fwrite(ByeData.data(), ByeData.size(), 1, F); + fwrite("\n\n", 2, 1, F); + fclose(F); + // Clean up the sub process + if (ExecWait(Process, Opts->Value.c_str()) == false) + { + result = _error->Error("Failure running hook %s", Opts->Value.c_str()); + break; + } + } + signal(SIGINT, old_sigint); + signal(SIGPIPE, old_sigpipe); + signal(SIGQUIT, old_sigquit); + + return result; +} diff --git a/apt-private/private-json-hooks.h b/apt-private/private-json-hooks.h new file mode 100644 index 000000000..41be2950e --- /dev/null +++ b/apt-private/private-json-hooks.h @@ -0,0 +1,14 @@ +/* + * private-json-hooks.h - 2nd generation, JSON-RPC, hooks for APT + * + * Copyright (c) 2018 Canonical Ltd + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <set> +#include <string> + +#include <apt-private/private-cachefile.h> + +bool RunJsonHook(std::string const &option, std::string const &method, const char **FileList, CacheFile &Cache, std::set<std::string> const &UnknownPackages = {}); diff --git a/apt-private/private-search.cc b/apt-private/private-search.cc index eac7abd05..52a52e522 100644 --- a/apt-private/private-search.cc +++ b/apt-private/private-search.cc @@ -12,7 +12,9 @@ #include <apt-pkg/policy.h> #include <apt-pkg/progress.h> +#include <apt-private/private-cachefile.h> #include <apt-private/private-cacheset.h> +#include <apt-private/private-json-hooks.h> #include <apt-private/private-output.h> #include <apt-private/private-search.h> #include <apt-private/private-show.h> @@ -29,7 +31,9 @@ static bool FullTextSearch(CommandLine &CmdL) /*{{{*/ { - pkgCacheFile CacheFile; + + CacheFile CacheFile; + CacheFile.GetDepCache(); pkgCache *Cache = CacheFile.GetPkgCache(); pkgDepCache::Policy *Plcy = CacheFile.GetPolicy(); if (unlikely(Cache == NULL || Plcy == NULL)) @@ -40,6 +44,8 @@ static bool FullTextSearch(CommandLine &CmdL) /*{{{*/ if (NumPatterns < 1) return _error->Error(_("You must give at least one search pattern")); + RunJsonHook("AptCli::Hooks::Search", "org.debian.apt.hooks.search.pre", CmdL.FileList, CacheFile); + #define APT_FREE_PATTERNS() for (std::vector<regex_t>::iterator P = Patterns.begin(); \ P != Patterns.end(); ++P) { regfree(&(*P)); } @@ -127,6 +133,10 @@ static bool FullTextSearch(CommandLine &CmdL) /*{{{*/ for (K = output_map.begin(); K != output_map.end(); ++K) std::cout << (*K).second << std::endl; + if (output_map.empty()) + RunJsonHook("AptCli::Hooks::Search", "org.debian.apt.hooks.search.fail", CmdL.FileList, CacheFile); + else + RunJsonHook("AptCli::Hooks::Search", "org.debian.apt.hooks.search.post", CmdL.FileList, CacheFile); return true; } /*}}}*/ |