summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.json-hooks.md159
-rw-r--r--apt-private/private-cacheset.cc10
-rw-r--r--apt-private/private-cacheset.h2
-rw-r--r--apt-private/private-install.cc29
-rw-r--r--apt-private/private-install.h2
-rw-r--r--apt-private/private-json-hooks.cc425
-rw-r--r--apt-private/private-json-hooks.h14
-rw-r--r--apt-private/private-search.cc12
-rwxr-xr-xtest/integration/test-apt-cli-json-hooks122
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