Browse Source

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.
tags/debian/1.6_rc1
Julian Andres Klode 3 years ago
parent
commit
e9796b9c21
7 changed files with 756 additions and 7 deletions
  1. +159
    -0
      README.json-hooks.md
  2. +24
    -5
      apt-private/private-install.cc
  3. +1
    -1
      apt-private/private-install.h
  4. +425
    -0
      apt-private/private-json-hooks.cc
  5. +14
    -0
      apt-private/private-json-hooks.h
  6. +11
    -1
      apt-private/private-search.cc
  7. +122
    -0
      test/integration/test-apt-cli-json-hooks

+ 159
- 0
README.json-hooks.md View File

@@ -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.

+ 24
- 5
apt-private/private-install.cc View File

@@ -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;
}
/*}}}*/



+ 1
- 1
apt-private/private-install.h View File

@@ -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);



+ 425
- 0
apt-private/private-json-hooks.cc View File

@@ -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;
}

+ 14
- 0
apt-private/private-json-hooks.h View File

@@ -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 = {});

+ 11
- 1
apt-private/private-search.cc View File

@@ -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;
}
/*}}}*/


+ 122
- 0
test/integration/test-apt-cli-json-hooks View File

@@ -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

Loading…
Cancel
Save