diff options
-rw-r--r-- | README.json-hooks.md | 159 | ||||
-rw-r--r-- | apt-private/private-cacheset.cc | 10 | ||||
-rw-r--r-- | apt-private/private-cacheset.h | 2 | ||||
-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 | ||||
-rwxr-xr-x | test/integration/test-apt-cli-json-hooks | 122 |
9 files changed, 765 insertions, 10 deletions
diff --git a/README.json-hooks.md b/README.json-hooks.md new file mode 100644 index 000000000..b47c33b7e --- /dev/null +++ b/README.json-hooks.md @@ -0,0 +1,159 @@ +## JSON Hooks + +APT 1.6 introduces support for hooks that talk JSON-RPC 2.0. Hooks act +as a server, and APT as a client. + +## Wire protocol + +APT communicates with hooks via a UNIX domain socket in file descriptor +`$APT_HOOK_SOCKET`. The transport is a byte stream (SOCK_STREAM). + +The byte stream contains multiple JSON objects, each representing a +JSON-RPC request or response, and each terminated by an empty line +(`\n\n`). Therefore, JSON objects containing empty lines may not be +used. + +For protocol version `0.1`, each JSON object must be encoded on a single +line. + +## Lifecycle + +The general life of a hook is as following. + +1. Hook is started +2. Hello handshake is exchanged +3. One or more calls or notifications are sent from apt to the hook +4. Bye notification is send + +It is unspecified whether a hook is sent one or more messages. For +example, a hook may be started only once for the lifetime of the apt +process and receive multiple notificatgions, but a hook may also be +started multiple times. Hooks should thus be stateless. + +## JSON messages + +### Hello handshake + +APT performs a call to the method `org.debian.apt.hooks.hello` with +the named parameter `versions` containing a list of supported protocol +versions. The hook picks the version it supports. The current version +is `"0.1"`, and support for that version is mandatory. + +*Example*: + +1. APT: + ```{"jsonrpc":"2.0","method":"org.debian.apt.hooks.hello","id":0,"params":{"versions":["0.1"]}}``` + + +2. Hook: + ```{"jsonrpc":"2.0","id":0,"result":{"version":"0.1"}}``` + +### Bye notification + +Before closing the connection, APT sends a notification for the +method `org.debian.apt.hooks.bye`. + +### Hook notification + +The following methods are supported: + +1. `org.debian.apt.hooks.install.pre-prompt` - Run before the y/n prompt +1. `org.debian.apt.hooks.install.post` - Run after success +1. `org.debian.apt.hooks.install.fail` - Run after failed instal +1. `org.debian.apt.hooks.search.pre` - Run before search +1. `org.debian.apt.hooks.search.post` - Run after successful search +1. `org.debian.apt.hooks.search.fail` - Run after search without results + +They can be registered by adding them to the list: + +```AptCli::Hooks::<name>``` + +where `<name>` is the name of the hook. It is recommended that these +option names are prefixed with `Binary::apt`, so that they only take +effect for the `apt` binary. Otherwise, there may be compatibility issues +with scripts and alike. + +#### Parameters + +*command*: The command used on the command-line. For example, `"purge"`. + +*search-terms*: Any non-option arguments given to the command. + +*unknown-packages*: For non-search hooks, a subset of *search-terms* +that APT could not find packages in the cache for. + +*packages*: An array of modified packages. This is mostly useful for +install. Each package has the following attributes: + +- *id*: An unsigned integer describing the package +- *name*: The name of the package +- *architecture*: The architecture of the package. For `"all"` packages, this will be the native architecture; + use per-version architecture fields to see `"all"`. + +- *mode*: One of `install`, `deinstall`, `purge`, or `keep`. `keep` + is not exposed in 0.1. To determine an upgrade, check + that a current version is installed. +- *automatic*: Whether the package is/will be automatically installed +- *versions*: An array with up to 3 fields: + + - *candidate*: The candidate version + - *install*: The version to be installed + - *current*: The version currently installed + + Each version is represented as an object with the following fields: + + - *id*: An unsigned integer + - *version*: The version as a string + - *architecture*: Architecture of the version + - *pin*: The pin priority + +#### Example + +```json +{ + "jsonrpc": "2.0", + "method": "org.debian.apt.hooks.install.pre", + "params": { + "command": "purge", + "search-terms": [ + "petname-", + "lxd+" + ], + "packages": [ + { + "id": 1500, + "name": "ebtables", + "architecture": "amd64", + "mode": "install", + "automatic": 1, + "versions": { + "candidate": { + "id": 376, + "version": "2.0.10.4-3.5ubuntu2", + "architecture": "amd64", + "pin": 990 + }, + "install": { + "id": 376, + "version": "2.0.10.4-3.5ubuntu2", + "architecture": "amd64", + "pin": 990 + } + } + } + ] + } +} +``` + +#### Compatibility note +Future versions of APT might make these calls instead of notifications. + +## Evolution of this protocol +New incompatible versions may be introduced with each new feature +release of apt (1.7, 1.8, etc). No backward compatibility is promised +until protocol 1.0: New stable feature releases may support a newer +protocol version only (for example, 1.7 may only support 0.2). + +Additional fields may be added to objects without bumping the protocol +version. diff --git a/apt-private/private-cacheset.cc b/apt-private/private-cacheset.cc index 3d1a2b91c..95a16f8ba 100644 --- a/apt-private/private-cacheset.cc +++ b/apt-private/private-cacheset.cc @@ -358,9 +358,15 @@ APT::VersionSet CacheSetHelperAPTGet::tryVirtualPackage(pkgCacheFile &Cache, pkg } pkgCache::PkgIterator CacheSetHelperAPTGet::canNotFindPkgName(pkgCacheFile &Cache, std::string const &str) { - pkgCache::PkgIterator const Pkg = canNotFindPkgName_impl(Cache, str); + pkgCache::PkgIterator Pkg = canNotFindPkgName_impl(Cache, str); if (Pkg.end()) - return APT::CacheSetHelper::canNotFindPkgName(Cache, str); + { + Pkg = APT::CacheSetHelper::canNotFindPkgName(Cache, str); + if (Pkg.end() && ShowError) + { + notFound.insert(str); + } + } return Pkg; } /*}}}*/ diff --git a/apt-private/private-cacheset.h b/apt-private/private-cacheset.h index 7bf486b9e..d20d00b68 100644 --- a/apt-private/private-cacheset.h +++ b/apt-private/private-cacheset.h @@ -94,9 +94,9 @@ class CacheSetHelperAPTGet : public APT::CacheSetHelper { bool explicitlyNamed; APT::PackageSet virtualPkgs; - public: std::list<std::pair<pkgCache::VerIterator, std::string> > selectedByRelease; + std::set<std::string> notFound; explicit CacheSetHelperAPTGet(std::ostream &out); 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; } /*}}}*/ diff --git a/test/integration/test-apt-cli-json-hooks b/test/integration/test-apt-cli-json-hooks new file mode 100755 index 000000000..0d2a55fb3 --- /dev/null +++ b/test/integration/test-apt-cli-json-hooks @@ -0,0 +1,122 @@ +#!/bin/sh +set -e + +TESTDIR="$(readlink -f "$(dirname "$0")")" +. "$TESTDIR/framework" + +setupenvironment +configarchitecture "i386" + +DESCR='Some description that has a unusual word xxyyzz and aabbcc and a UPPERCASE' +DESCR2='Some other description with the unusual aabbcc only' +insertpackage 'unstable' 'foo' 'all' '1.0' '' '' "$DESCR + Long description of stuff and such, with lines + . + and paragraphs and everything." +insertpackage 'testing' 'bar' 'i386' '2.0' '' '' "$DESCR2" + +setupaptarchive + +APTARCHIVE="$(readlink -f ./aptarchive)" + +cat >> json-hook.sh << EOF +#!/bin/bash +while true; do + read request <&\$APT_HOOK_SOCKET + read empty <&\$APT_HOOK_SOCKET + + if echo "\$request" | grep -q ".hello"; then + echo "HOOK: HELLO" + printf '{"jsonrpc": "2.0", "result": {"version": "0.1"}, "id": 0}\n\n' >&\$APT_HOOK_SOCKET + fi + + if echo "\$request" | grep -q ".bye"; then + echo "HOOK: BYE" + exit 0; + fi + + echo HOOK: request \$request + echo HOOK: empty \$empty +done +EOF + +chmod +x json-hook.sh + +HOOK="$(readlink -f ./json-hook.sh)" + +# Setup all hooks +cat >> rootdir/etc/apt/apt.conf.d/99-json-hooks << EOF + AptCli::Hooks::Install:: "$HOOK"; + AptCli::Hooks::Search:: "$HOOK"; +EOF + + +############################# Success search ####################### +testsuccessequal 'HOOK: HELLO +HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.hello","id":0,"params":{"versions":["0.1"]}} +HOOK: empty +HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.search.pre","params":{"command":"search","search-terms":["foo"],"unknown-packages":[],"packages":[]}} +HOOK: empty +HOOK: BYE +Sorting... +Full Text Search... +foo/unstable 1.0 all + Some description that has a unusual word xxyyzz and aabbcc and a UPPERCASE + +HOOK: HELLO +HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.hello","id":0,"params":{"versions":["0.1"]}} +HOOK: empty +HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.search.post","params":{"command":"search","search-terms":["foo"],"unknown-packages":[],"packages":[]}} +HOOK: empty +HOOK: BYE' apt search foo + +############################# Failed search ####################### +testsuccessequal 'HOOK: HELLO +HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.hello","id":0,"params":{"versions":["0.1"]}} +HOOK: empty +HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.search.pre","params":{"command":"search","search-terms":["foox"],"unknown-packages":[],"packages":[]}} +HOOK: empty +HOOK: BYE +Sorting... +Full Text Search... +HOOK: HELLO +HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.hello","id":0,"params":{"versions":["0.1"]}} +HOOK: empty +HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.search.fail","params":{"command":"search","search-terms":["foox"],"unknown-packages":[],"packages":[]}} +HOOK: empty +HOOK: BYE' apt search foox + + +############################# Failed install ####################### + +testfailureequal 'Reading package lists... +Building dependency tree... +HOOK: HELLO +HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.hello","id":0,"params":{"versions":["0.1"]}} +HOOK: empty +HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.install.fail","params":{"command":"install","search-terms":["foxxx"],"unknown-packages":["foxxx"],"packages":[]}} +HOOK: empty +HOOK: BYE +E: Unable to locate package foxxx' apt install foxxx + +############################# Success install ####################### + +testsuccessequal 'Reading package lists... +Building dependency tree... +HOOK: HELLO +HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.hello","id":0,"params":{"versions":["0.1"]}} +HOOK: empty +HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.install.pre-prompt","params":{"command":"install","search-terms":["foo"],"unknown-packages":[],"packages":[{"id":1,"name":"foo","architecture":"i386","mode":"install","automatic":false,"versions":{"candidate":{"id":1,"version":"1.0","architecture":"all","pin":500},"install":{"id":1,"version":"1.0","architecture":"all","pin":500}}}]}} +HOOK: empty +HOOK: BYE +The following NEW packages will be installed: + foo +0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded. +Inst foo (1.0 unstable [all]) +Conf foo (1.0 unstable [all]) +HOOK: HELLO +HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.hello","id":0,"params":{"versions":["0.1"]}} +HOOK: empty +HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.install.post","params":{"command":"install","search-terms":["foo"],"unknown-packages":[],"packages":[{"id":1,"name":"foo","architecture":"i386","mode":"install","automatic":false,"versions":{"candidate":{"id":1,"version":"1.0","architecture":"all","pin":500},"install":{"id":1,"version":"1.0","architecture":"all","pin":500}}}]}} +HOOK: empty +HOOK: BYE' apt install foo -s |