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

Commit

Permalink
kdb cli: add support for external commands, with and without spec
Browse files Browse the repository at this point in the history
  • Loading branch information
hannes99 committed Jun 8, 2023
1 parent 212ee65 commit 7a12dd5
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 1 deletion.
4 changes: 4 additions & 0 deletions doc/news/_preparation_next_release.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,10 @@ This section keeps you up-to-date with the multi-language support provided by El
- _NOTE_: The `path` argument for `kdb complete` is now required, so instead of `kdb complete` `kdb complete ""` has to be used. The
reason for this is described in issue #4952.
- <<TODO>>
- add support for external commands, with and without spec _(@hannes99)_
- It is still possible to execute external binaries that are placed in a specific director, same as it worked in the old implementation.
- It is now possible to provide a specification to external commands to `kdb`, so `kdb` check/parses arguments and then calls the
external program with them.

### <<Tool>>

Expand Down
1 change: 1 addition & 0 deletions doc/tutorials/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Read this first to get the basic concepts of Elektra.
- [Cascading](cascading.md)
- [Arrays](arrays.md)
- [Mount Configuration Files](mount.md)
- [External KDB commands](external-commands.md)

## Developers

Expand Down
57 changes: 57 additions & 0 deletions doc/tutorials/external-commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# How-to: Add external commands

This tutorial will describe how to provide `kdb` with the specification of external programs.
So `kdb` can parse and check the provided options and arguments according to the provided specification.
This allows you to have, for example, a shell script but its args are checked by `kdb` before running it.
It is possible to either mount(`kdb mount`) the specification, or set the keys manually using `kdb set` and `kdb meta-set`.
Both options will be described in the following.

For a reference of how the specification can look like [Command Line Options](command-line-options.md).

## With `kdb mount`

```ni
[file]
meta:/description = the file that shall be deleted
meta:/args = indexed
meta:/args/index = 0
[]
meta:/command = somecommand
meta:/description = Simple script
meta:/external = 1
meta:/bin = /path/to/test.sh
```

The file then has to be mounted with

```sh
kdb mount /path/to/spec.ni spec:/sw/elektra/kdb/#0/current/somecommand mini
```

## Alternative to `kdb mount`

This is the same as mounting the spec file.

```bash
kdb set spec:/sw/elektra/kdb/#0/current/somecommand ""
kdb meta-set spec:/sw/elektra/kdb/#0/current/somecommand external 1
kdb meta-set spec:/sw/elektra/kdb/#0/current/somecommand bin "/path/to/test.sh"
kdb meta-set spec:/sw/elektra/kdb/#0/current/somecommand command "somecommand"
kdb meta-set spec:/sw/elektra/kdb/#0/current/somecommand description "Simple script"
kdb set spec:/sw/elektra/kdb/#0/current/somecommand ""
kdb meta-set spec:/sw/elektra/kdb/#0/current/somecommand/file description "the file that shall be deleted"
kdb meta-set spec:/sw/elektra/kdb/#0/current/somecommand/file args indexed
kdb meta-set spec:/sw/elektra/kdb/#0/current/somecommand/file args/index 0
```

> **_NOTE:_** Extra arguments are directly passed on to the external command. So it is possible to provide the external program with more
> args than specified in the spec. Those are not check by `KDB`.
So basically keys in `spec:/sw/elektra/kdb/#0/current/..` are considered external commands as long as the metakey `external` is set to 1
and a metakey `bin`, that has the path to the binary, is set. Instead of mounting the spec file it is also possible to the set spec
manually using `kdb`.

`bin` should be an absolut path. If it is not, the binary will be search relative to where `kdb` is executed.

External commands specified like this will appear in `kdb --help` and can be used with `kdb somecommand`.
196 changes: 196 additions & 0 deletions src/tools/kdb/external.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/**
* @file
*
* @brief Code to support external programs
*
* @copyright BSD License (see LICENSE.md or https://www.libelektra.org)
*/

#include <command.h>
#include <external.h>
#include <kdbease.h>

#include <errno.h>
#include <kdberrors.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

#ifdef _WIN32
#include <windows.h>
#include <winsock2.h>
#else
#include <spawn.h>
#include <sys/wait.h>
#endif

extern char ** environ;

const char * getExternalBin (KeySet * binaries, const char * key)
{
Key * tmp = keyNew ("/", KEY_END);
keySetBaseName (tmp, key);
Key * resultKey = ksLookup (binaries, tmp, KDB_O_NONE);
keyDel (tmp);
if (resultKey == NULL)
{
return NULL;
}
return keyString (resultKey);
}


int tryLoadExternal (char * commandName, KeySet * binaries)
{
char * execPathPtr = getenv ("KDB_EXEC_PATH");
bool found = false;
char path[PATH_MAX] = { 0 };
char * saveptr;
struct stat buf;

if (execPathPtr)
{
char * execPath = strdup (execPathPtr);
char * token = strtok_r (execPath, ":", &saveptr);
while (token != NULL && !found)
{
snprintf (path, sizeof (path), "%s/%s", token, commandName);
found = stat (path, &buf) != -1;
token = strtok_r (NULL, ":", &saveptr);
}
elektraFree (execPath);
}

if (!found)
{
snprintf (path, sizeof (path), "%s/%s", BUILTIN_EXEC_FOLDER, commandName);
found = stat (path, &buf) != -1;
}

if (found)
{
Key * tmp = keyNew ("/", KEY_END);
keySetBaseName (tmp, commandName);
keySetString (tmp, path);
ksAppendKey (binaries, tmp);
return 0;
}
return 1;
}

int loadExternalSpec (KeySet * spec, KeySet * binaries, Key * errorKey)
{
KDB * handle = kdbOpen (NULL, errorKey);
Key * baseKey = keyNew ("spec:" CLI_BASE_KEY, KEY_END);
KeySet * config = ksNew (0, KS_END);
if (kdbGet (handle, config, errorKey) == -1)
{
ELEKTRA_SET_VALIDATION_SEMANTIC_ERRORF (errorKey, "could not load '%s': %s", CLI_BASE_KEY, GET_ERR (baseKey));
keyDel (baseKey);
ksDel (config);
kdbClose (handle, errorKey);
return 1;
}
Key * cur = NULL;
KeySet * part = ksCut (config, baseKey);

for (elektraCursor it = 0; it < ksGetSize (part); ++it)
{
cur = ksAtCursor (part, it);
const Key * externalMeta = keyGetMeta (cur, "external");
const Key * externalBinary = keyGetMeta (cur, "bin");
bool isExternal = false;
if (externalBinary != NULL && externalMeta != NULL && elektraKeyToBoolean (externalMeta, &isExternal) && isExternal)
{ // add external spec and save path to binary
KeySet * externalCommandSpec = ksCut (part, cur);

Key * tmp = keyNew ("/", KEY_END);
keySetBaseName (tmp, keyBaseName (cur));
keySetString (tmp, keyString (externalBinary));
ksAppendKey (binaries, tmp);

ksAppend (spec, externalCommandSpec);
ksDel (externalCommandSpec);
it--;
}
}
ksDel (part);
ksDel (config);
kdbClose (handle, errorKey);
keyDel (baseKey);
return 0;
}

int runExternal (const char * bin, char ** argv, Key * errorKey)
{
// the external program should think it was called directly
argv[1] = (char *) bin;

int status = 0;

#ifdef _WIN32
STARTUPINFO si;
PROCESS_INFORMATION pi;

ZeroMemory (&si, sizeof (si));
si.cb = sizeof (si);
ZeroMemory (&pi, sizeof (pi));

// Construct command line string
char cmdline[MAX_PATH] = "";
for (int i = 1; argv[i]; ++i)
{
strcat (cmdline, "\"");
strcat (cmdline, argv[i]);
strcat (cmdline, "\" ");
}

// Start the child process.
if (!CreateProcess (NULL, // Module name
cmdline, // Command line
NULL, // Process handle not inheritable
NULL, // Thread handle not inheritable
FALSE, // Set handle inheritance to FALSE
0, // No creation flags
NULL, // Use parent's environment block
NULL, // Use parent's starting directory
&si, // Pointer to STARTUPINFO structure
&pi) // Pointer to PROCESS_INFORMATION structure
)
{
ELEKTRA_SET_RESOURCE_ERRORF (errorKey, "CreateProcess failed: %lu", GetLastError ());
return 1;
}

// Wait until child process exits.
WaitForSingleObject (pi.hProcess, INFINITE);

// Get exit code
DWORD exitCode;
GetExitCodeProcess (pi.hProcess, &exitCode);
status = (int) exitCode;

// Close process and thread handles.
CloseHandle (pi.hProcess);
CloseHandle (pi.hThread);
#else
pid_t pid;

if (posix_spawn (&pid, bin, NULL, NULL, &(argv[1]), environ) != 0)
{
ELEKTRA_SET_RESOURCE_ERRORF (errorKey, "posix_spawn failed: %s", strerror (errno));
return 1;
}

if (waitpid (pid, &status, 0) < 0)
{
ELEKTRA_SET_RESOURCE_ERRORF (errorKey, "waitpid failed: %s", strerror (errno));
return 1;
}
#endif

fflush (stdout);
return status;
}
20 changes: 20 additions & 0 deletions src/tools/kdb/external.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @file
*
* @brief Header for things needed for external programs
*
* @copyright BSD License (see LICENSE.md or https://www.libelektra.org)
*/

#ifndef ELEKTRA_KDB_EXTERNAL_H
#define ELEKTRA_KDB_EXTERNAL_H

#include <kdb.h>

const char * getExternalBin (KeySet * binaries, const char * key);

int runExternal (const char * bin, char ** argv, Key * errorKey);
int loadExternalSpec (KeySet * spec, KeySet * binaries, Key * errorKey);
int tryLoadExternal (char * commandName, KeySet * binaries);

#endif // ELEKTRA_KDB_EXTERNAL_H
27 changes: 26 additions & 1 deletion src/tools/kdb/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
#include <validate.h>

#include <command.h>
#include <external.h>
#include <kdb.h>
#include <kdbhelper.h>
#include <kdbopts.h>
Expand Down Expand Up @@ -205,6 +206,10 @@ int main (int argc, char ** argv)
"kdb is a program to manage Elektra's key database.", KEY_END),
KS_END);

// external programs with spec in spec:/sw/elektra/kdb/#0/current/<cmd>
KeySet * externalBinaries = ksNew (10, KS_END);
loadExternalSpec (options, externalBinaries, parentKey);

// C spec
for (unsigned long i = 0; i < sizeof (subcommands) / sizeof (subcommands[0]); ++i)
{
Expand All @@ -225,14 +230,22 @@ int main (int argc, char ** argv)
printWarnings (parentKey);
keyDel (parentKey);
ksDel (options);
ksDel (externalBinaries);
return 0;
}
if (result == -1)
{ // error
const char * errorMessage = GET_ERR (parentKey);
if (elektraStrNCmp (errorMessage, "Unknown sub-command:", 20) == 0)
{
result = cpp_main (argc, argv);
if (tryLoadExternal (argv[1], externalBinaries) == 0)
{
result = 0;
}
else
{
result = 4;
}
}
else if (elektraStrNCmp (errorMessage, "Unknown short option:", 21) == 0 ||
elektraStrNCmp (errorMessage, "Unknown long option:", 20) == 0)
Expand All @@ -252,6 +265,17 @@ int main (int argc, char ** argv)

const char * subcommand = keyString (ksLookupByName (options, CLI_BASE_KEY, 0));

// external
const char * externalBin = getExternalBin (externalBinaries, argv[1]);
if (externalBin != NULL)
{
Key * errorKey = keyNew (CLI_SPEC_KEY, KEY_END);
result = runExternal (externalBin, argv, errorKey);
printError (errorKey);
keyDel (errorKey);
goto cleanup;
}

// C
for (unsigned long i = 0; i < sizeof (subcommands) / sizeof (command); ++i)
{
Expand Down Expand Up @@ -279,5 +303,6 @@ int main (int argc, char ** argv)
cleanup:
keyDel (parentKey);
ksDel (options);
ksDel (externalBinaries);
return result;
}

0 comments on commit 7a12dd5

Please sign in to comment.