Skip to content
This repository has been archived by the owner on Oct 15, 2024. It is now read-only.

Commit

Permalink
Merge pull request #4471 from atmaxinger/nb-hooks
Browse files Browse the repository at this point in the history
Basic implementation of "gopts" hook
  • Loading branch information
markus2330 authored Sep 24, 2022
2 parents 3e9f5ed + 56ff3b2 commit fac8496
Show file tree
Hide file tree
Showing 15 changed files with 475 additions and 25 deletions.
2 changes: 1 addition & 1 deletion doc/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ section here.

## In Progress

- [Global Plugins](global_plugins.md) (@mpranj)
- [Hooks](hooks.md) (@atmaxinger)
- [Ensure](ensure.md) (@kodebach)
- [Capabilities](capabilities.md) (@markus2330)
- [Error Semantics](error_semantics.md) (API)
Expand Down
2 changes: 1 addition & 1 deletion doc/decisions/array.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ The `spec` plugin should check if it is a valid array, i.e.:

## Related Decisions

- [Global Plugins](global_plugins.md)
- [Hooks](hooks.md)
- [Global Validation](global_validation.md)
- [Base Names](base_name.md)
- [Metadata in Spec Namespace](spec_metadata.md)
Expand Down
2 changes: 1 addition & 1 deletion doc/decisions/ensure.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,6 @@ invocations. Contract `KeySet`s only contain `Key`s below
## Related Decisions
- [Global Plugins](global_plugins.md)
- [Hooks](hooks.md)
## Notes
18 changes: 15 additions & 3 deletions doc/decisions/global_plugins.md → doc/decisions/hooks.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
# Global Plugins
# Hooks in KDB

## Problem

Some components of `kdbGet`/`kdbSet` should be optional.
We use the plugin system for that.
However, some of these cases cannot be tied to a mountpoint.
This was the idea of global plugins, but that idea proved problematic.

In the old global plugins implementation:

- Notification does not happen once after final commit, but for every
plugin
- Problems in spec plugin
Expand All @@ -11,21 +18,23 @@ We need to clean up and simplify the placement.

## Constraints

- Plugin interface should be the same. Many plugins, e.g. dbus, should work
- Plugin interface should be the same. Many plugins, where appropriate, e.g. dbus, should work
as global plugins w/o any change in code (i.e. only changes
in contract)

- Global plugins might depend on specific applications or specific
mount points (it should be possible to enforce global plugins for specific
applications).

## Assumptions

- Elektra is useful with following types of global plugins:
- Elektra is useful with following types of plugins:
- mmap
- spec
- gopts
- receiving of notifications (internalnotification)
- sending of notifications (dbus, ...)
- recording of changes
- There are not too many types of global plugins, not more than 10

## Considered Alternatives
Expand All @@ -44,6 +53,8 @@ These hooks are not shared, so no `list` plugin is needed.

Installed plugins will be used.

In the beginning, we'll hardcode the names of the plugins. For changing those plugins symlinks will have to be used.

## Rationale

- allows adding more types of plugins later, also post-1.0
Expand All @@ -57,6 +68,7 @@ Installed plugins will be used.
- remove `list` plugin
- remove plugins that stop working or disallow global positioning for them
- call `spec` as needed several times
- remove current global plugins mechanism

## Related Decisions

Expand Down
24 changes: 24 additions & 0 deletions doc/dev/hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Hooks

Hooks are central points in the KDB lifecycle, where specialized plugins are called.

## Selecting which Plugin will be Used for a Specific Hook

Currently, the names of the plugins are hard-coded.
This [decision](../decisions/hooks.md) was made, because these plugins are meant to fulfil very specific purposes.
A symlink replacing the shared library file of the plugin could be used to change the implementation.

## Interface of the hooks

If a plugin should be able to act upon a hook, it must export all the functions that the hook requires.
These exports are of the form `system:/elektra/modules/<plugin name>/exports/hook/<hook name>/<hook function>`.

For example, the `gopts` hook only requires the `get` function. A plugin that wants to act upon the `gopts` hook therefore has to export `system:/elektra/modules/<plugin name>/exports/hook/gopts/get`.

Other hooks (e.g. `spec`) require multiple exported functions.

## Lifecycle

1. Hooks are initilized within `kdbOpen` after the contract has been processed. This includes loading the plugins.
2. The appropriate hooks are called within each `kdbGet` and `kdbSet` call.
3. Hooks are deinitialized within `kdbClose`. This includes unloading the plugins.
2 changes: 1 addition & 1 deletion doc/news/_preparation_next_release.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ _(Michael Tucek)_
### New Backend

- Improve [description of plugin framework](doc/dev/plugins-framework.md).
- <<TODO>>
- Implement [hooks](doc/decisions/hooks.md). _(Maximilian Irlinger @atmaxinger)_
- <<TODO>>
- <<TODO>>
- <<TODO>>
Expand Down
15 changes: 14 additions & 1 deletion src/include/kdbprivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ typedef int (*kdbSetPtr) (Plugin * handle, KeySet * returned, Key * parentKey);
typedef int (*kdbErrorPtr) (Plugin * handle, KeySet * returned, Key * parentKey);
typedef int (*kdbCommitPtr) (Plugin * handle, KeySet * returned, Key * parentKey);

typedef int (*kdbHookGoptsGetPtr) (Plugin * handle, KeySet * returned, Key * parentKey);

typedef Plugin * (*OpenMapper) (const char *, const char *, KeySet *);
typedef int (*CloseMapper) (Plugin *);

Expand Down Expand Up @@ -319,6 +321,7 @@ struct _KeySet
#endif
};

typedef struct _Hooks Hooks;

/**
* The access point to the key database.
Expand Down Expand Up @@ -362,8 +365,16 @@ struct _KDB
up their parts of the global keyset, which they do not need any more.*/

Plugin * globalPlugins[NR_GLOBAL_POSITIONS][NR_GLOBAL_SUBPOSITIONS];

KeySet * backends;

struct
{
struct
{
struct _Plugin* plugin;
kdbHookGoptsGetPtr kdbHookGoptsGet;
} gopts;
} hooks;
};

/**
Expand Down Expand Up @@ -564,6 +575,8 @@ int mountModules (KDB * kdb, KeySet * modules, Key * errorKey);
int mountVersion (KDB * kdb, Key * errorKey);
int mountGlobals (KDB * kdb, KeySet * keys, KeySet * modules, Key * errorKey);
int mountBackend (KDB * kdb, const Key * mountpoint, Plugin * backend);
int initHooks (KDB * kdb, const KeySet * config, KeySet * modules, const KeySet * contract, Key * errorKey);
void freeHooks (KDB * kdb, Key * errorKey);

const Key * mountGetMountpoint (KDB * handle, Key * where);
Plugin * mountGetBackend (KDB * handle, Key * key);
Expand Down
1 change: 1 addition & 0 deletions src/libs/elektra/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ if (BUILD_SHARED)
backend.c
kdb.c
mount.c
hooks.c
split.c
trie.c
plugin.c
Expand Down
6 changes: 3 additions & 3 deletions src/libs/elektra/global.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ int elektraGlobalGet (KDB * handle, KeySet * ks, Key * parentKey, int position,
Plugin * plugin;
if (handle && (plugin = handle->globalPlugins[position][subPosition]))
{
ret = plugin->kdbGet (plugin, ks, parentKey);
// ret = plugin->kdbGet (plugin, ks, parentKey);
}
return ret;
}
Expand All @@ -31,7 +31,7 @@ int elektraGlobalSet (KDB * handle, KeySet * ks, Key * parentKey, int position,
Plugin * plugin;
if (handle && (plugin = handle->globalPlugins[position][subPosition]))
{
ret = plugin->kdbSet (plugin, ks, parentKey);
// ret = plugin->kdbSet (plugin, ks, parentKey);
}
return ret;
}
Expand All @@ -42,7 +42,7 @@ int elektraGlobalError (KDB * handle, KeySet * ks, Key * parentKey, int position
Plugin * plugin;
if (handle && (plugin = handle->globalPlugins[position][subPosition]))
{
ret = plugin->kdbError (plugin, ks, parentKey);
// ret = plugin->kdbError (plugin, ks, parentKey);
}
return ret;
}
190 changes: 190 additions & 0 deletions src/libs/elektra/hooks.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/**
* @file
*
* @brief
*
* @copyright BSD License (see LICENSE.md or https://www.libelektra.org)
*/

#include <kdbinternal.h>

/**
* Uninitializes and frees all hooks in the passed KDB handle.
*
* @param kdb the KDB handle where the hooks should be freed
* @param errorKey the key which holds errors and warnings which were issued
*/
void freeHooks (KDB * kdb, Key * errorKey)
{
if (kdb->hooks.gopts.plugin != NULL)
{
elektraPluginClose (kdb->hooks.gopts.plugin, errorKey);
}
}

static size_t getFunction (Plugin * plugin, const char * functionName, Key * errorKey)
{
size_t result = elektraPluginGetFunction (plugin, functionName);

if (result == 0)
{
ELEKTRA_ADD_INSTALLATION_WARNINGF (errorKey, "Plugin '%s' does not implement function '%s'", plugin->name, functionName);
}

return result;
}

static int initHooksGopts (KDB * kdb, Plugin * plugin, Key * errorKey)
{
if (!plugin)
{
return -1;
}

kdb->hooks.gopts.plugin = plugin;

if ((kdb->hooks.gopts.kdbHookGoptsGet = (kdbHookGoptsGetPtr) getFunction (plugin, "hook/gopts/get", errorKey)) == NULL)
{
return -1;
}

return 0;
}

/**
* Extracts the config for a single plugin from the contract
*
* The config must be below system:/elektra/contract/mountglobal/<pluginName> and will be moved to below user:/.
* It also must contain the key system:/elektra/contract/mountglobal/<pluginName>.
*
* @param pluginName The name of the plugin for which the config will be extracted
* @param contract The contract from which the config is extracted
*/
static KeySet * getPluginConfigFromContract (const char * pluginName, const KeySet * contract)
{
KeySet * tmpContract = ksDup (contract);
KeySet * config = ksNew (0, KS_END);

Key * mountContractRoot = keyNew ("system:/elektra/contract/mountglobal", KEY_END);
Key * pluginConfigRoot = keyNew ("user:/", KEY_END);

for (elektraCursor it = ksFindHierarchy (tmpContract, mountContractRoot, NULL); it < ksGetSize (tmpContract); it++)
{
Key * cur = ksAtCursor (tmpContract, it);
if (keyIsDirectlyBelow (mountContractRoot, cur) == 1)
{
const char * pluginNameOfConfig = keyBaseName (cur);

// only handle config for the specified plugin
// we might be able to replace this check by modifying the key mountContractRoot,
// but I just copied this function from the previous global plugins implementation
// and it works for now.
if (strcmp (pluginName, pluginNameOfConfig) != 0)
{
break;
}

KeySet * pluginConfig = ksCut (tmpContract, cur);

// increment ref count, because cur is part of pluginConfig and
// we hold a reference to cur that is still needed (via pluginName)
keyIncRef (cur);
ksRename (pluginConfig, cur, pluginConfigRoot);
ksAppend (config, pluginConfig);

// we need to delete cur separately, because it was ksCut() from contract
// we also need to decrement the ref count, because it was incremented above
keyDecRef (cur);
keyDel (cur);

--it;
}
}

keyDel (mountContractRoot);
keyDel (pluginConfigRoot);
ksDel (tmpContract);

return config;
}

static Plugin * loadPlugin (const char * pluginName, KeySet * global, KeySet * modules, const KeySet * contract, Key * errorKey)
{
Key * openKey = keyDup (errorKey, KEY_CP_ALL);

KeySet * config = getPluginConfigFromContract (pluginName, contract);

Plugin * plugin = elektraPluginOpen (pluginName, modules, config, openKey);

if (!plugin)
{
ELEKTRA_ADD_INSTALLATION_WARNINGF (errorKey, "Could not load plugin '%s'", pluginName);
keyCopyAllMeta (errorKey, openKey);
keyDel (openKey);
return NULL;
}

plugin->global = global;

return plugin;
}

static bool isGoptsEnabledByContract (const KeySet * contract)
{
KeySet * dupContract = ksDup (contract); // We need to duplicate because contract is const, and ksLookupByName doesn't take const
bool isEnabled = ksLookupByName (dupContract, "system:/elektra/contract/mountglobal/gopts", 0) != NULL;
ksDel (dupContract);

return isEnabled;
}

/**
* Initializes the hooks stored in the passed KDB handle.
* If the handle already contains initialized hooks, they will be reinitialized, including unloading and loading of their plugins.
* Parameters @p config and @p contract will be used to determine which hooks to populate.
*
* @param kdb the KDB instance where the hooks should be initialized
* @param config KeySet containing the current config in @p system:/elektra namespace
* @param modules the current list of loaded modules
* @param contract the contract passed to @p kdbOpen
* @param errorKey the key which holds errors and warnings which were issued
* @return 0 on success, -1 on failure
*/
int initHooks (KDB * kdb, const KeySet * config, KeySet * modules, const KeySet * contract, Key * errorKey)
{
bool existingError = ksLookupByName (keyMeta (errorKey), "meta:/error", 0) != NULL;

if (!existingError)
{
// set a dummy value to block errors
// any errors that occur will be converted into warnings
keySetMeta (errorKey, "meta:/error", "blocked");
}

freeHooks (kdb, errorKey);

if (isGoptsEnabledByContract (contract) &&
initHooksGopts (kdb, loadPlugin ("gopts", kdb->global, modules, contract, errorKey), errorKey) != 0)
{
goto error;
}

if (!existingError)
{
// remove dummy error again
keySetMeta (errorKey, "meta:/error", NULL);
}

return 0;

error:
freeHooks (kdb, errorKey);

if (!existingError)
{
// remove dummy error again
keySetMeta (errorKey, "meta:/error", NULL);
}

return -1;
}
Loading

0 comments on commit fac8496

Please sign in to comment.