From e9796b9c21ee7d8e8f5d6e2a24db43fc4368b557 Mon Sep 17 00:00:00 2001 From: Julian Andres Klode Date: Fri, 13 Apr 2018 12:32:35 +0200 Subject: 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. --- apt-private/private-install.cc | 29 ++- apt-private/private-install.h | 2 +- apt-private/private-json-hooks.cc | 425 ++++++++++++++++++++++++++++++++++++++ apt-private/private-json-hooks.h | 14 ++ apt-private/private-search.cc | 12 +- 5 files changed, 475 insertions(+), 7 deletions(-) create mode 100644 apt-private/private-json-hooks.cc create mode 100644 apt-private/private-json-hooks.h (limited to 'apt-private') 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 #include #include +#include #include #include @@ -569,10 +570,11 @@ bool DoCacheManipulationFromCommandLine(CommandLine &CmdL, CacheFile &Cache, int bool DoCacheManipulationFromCommandLine(CommandLine &CmdL, std::vector &VolatileCmdL, CacheFile &Cache, int UpgradeMode) { std::map verset; - return DoCacheManipulationFromCommandLine(CmdL, VolatileCmdL, Cache, verset, UpgradeMode); + std::set UnknownPackages; + return DoCacheManipulationFromCommandLine(CmdL, VolatileCmdL, Cache, verset, UpgradeMode, UnknownPackages); } bool DoCacheManipulationFromCommandLine(CommandLine &CmdL, std::vector &VolatileCmdL, CacheFile &Cache, - std::map &verset, int UpgradeMode) + std::map &verset, int UpgradeMode, std::set &UnknownPackages) { // Enter the special broken fixing mode if the user specified arguments bool BrokenFix = false; @@ -621,6 +623,8 @@ bool DoCacheManipulationFromCommandLine(CommandLine &CmdL, std::vectorPendingError() == true) { helper.showVirtualPackageErrors(Cache); @@ -726,8 +730,13 @@ bool DoInstall(CommandLine &CmdL) return false; std::map verset; - if(!DoCacheManipulationFromCommandLine(CmdL, VolatileCmdL, Cache, verset, 0)) + std::set 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 &VolatileCmdL, CacheFile &Cache, - std::map &verset, int UpgradeMode); + std::map &verset, int UpgradeMode, std::set &UnknownPackages); bool DoCacheManipulationFromCommandLine(CommandLine &CmdL, std::vector &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 +#include +#include + +#include +#include +#include + +#include +#include +#include + +/** + * @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 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 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 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 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 +#include + +#include + +bool RunJsonHook(std::string const &option, std::string const &method, const char **FileList, CacheFile &Cache, std::set 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 #include +#include #include +#include #include #include #include @@ -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::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; } /*}}}*/ -- cgit v1.2.3