Merge branch 'hooks' into 2.4.0

This commit is contained in:
Paul Beckingham 2014-05-16 19:02:53 -04:00
commit de34095eb5
11 changed files with 381 additions and 139 deletions

View file

@ -1,5 +1,5 @@
cmake_minimum_required (VERSION 2.8)
install (DIRECTORY bash fish vim zsh
install (DIRECTORY bash fish vim zsh hooks
DESTINATION ${TASK_DOCDIR}/scripts)
install (DIRECTORY add-ons
DESTINATION ${TASK_DOCDIR}/scripts

23
scripts/hooks/on-add Executable file
View file

@ -0,0 +1,23 @@
#!/bin/bash
# Input:
# - New task JSON.
read new_task
# Processing goes here.
# Output:
# - Any line of JSON emitted is added as a new task.
# - Any non-JSON emitted is displayed as a message.
echo on-add
# Exit:
# - 0 Means accept $new_task if JSON is not emitted.
# - 0 Means accept all JSON as new tasks. If UUID matches $new_task, use this
# JSON instead of $new_task. If UUID does not match $new_task, then these
# are additional tasks.
# - 0 Means all non-JSON becomes footnote entries.
# - 1 Means all non-JSON becomes error entries.
# - 1 Means reject $new_task.
exit 0

22
scripts/hooks/on-exit Executable file
View file

@ -0,0 +1,22 @@
#!/bin/bash
# Input:
# - None
# Processing goes here.
# Output:
# - Any line of JSON emitted is added as a new task.
# - Any non-JSON emitted is displayed as a message.
echo on-exit
# Exit:
# - 0 Means accept $new_task if JSON is not emitted.
# - 0 Means accept all JSON as new tasks. If UUID matches $new_task, use this
# JSON instead of $new_task. If UUID does not match $new_task, then these
# are additional tasks.
# - 0 Means all non-JSON becomes footnote entries.
# - 1 Means all non-JSON becomes error entries.
# - 1 Means reject $new_task.
exit 0

22
scripts/hooks/on-launch Executable file
View file

@ -0,0 +1,22 @@
#!/bin/bash
# Input:
# - None
# Processing goes here.
# Output:
# - Any line of JSON emitted is added as a new task.
# - Any non-JSON emitted is displayed as a message.
echo on-launch
# Exit:
# - 0 Means accept $new_task if JSON is not emitted.
# - 0 Means accept all JSON as new tasks. If UUID matches $new_task, use this
# JSON instead of $new_task. If UUID does not match $new_task, then these
# are additional tasks.
# - 0 Means all non-JSON becomes footnote entries.
# - 1 Means all non-JSON becomes error entries.
# - 1 Means reject $new_task.
exit 0

25
scripts/hooks/on-modify Executable file
View file

@ -0,0 +1,25 @@
#!/bin/bash
# Input:
# - Original task JSON
# - Modified task JSON
read original_task
read modified_task
# Processing goes here.
# Output:
# - Any line of JSON emitted is added as a new task.
# - Any non-JSON emitted is displayed as a message.
echo on-modify
# Exit:
# - 0 Means accept $new_task if JSON is not emitted.
# - 0 Means accept all JSON as new tasks. If UUID matches $new_task, use this
# JSON instead of $new_task. If UUID does not match $new_task, then these
# are additional tasks.
# - 0 Means all non-JSON becomes footnote entries.
# - 1 Means all non-JSON becomes error entries.
# - 1 Means reject $new_task.
exit 0

View file

@ -566,6 +566,9 @@ void Config::createDefaultData (const std::string& data)
throw std::string ("Error: rc.data.location does not exist - exiting according to rc.exit.on.missing.db setting.");
d.create ();
d += "hooks";
d.create ();
}
}

View file

@ -221,10 +221,9 @@ int Context::initialize (int argc, const char** argv)
// Initialize the database.
tdb2.set_location (data_dir);
// Hook system init, plus post-start event occurring at the first possible
// moment after hook initialization.
// First opportunity to run a hook script.
hooks.initialize ();
hooks.trigger ("on-launch");
hooks.onLaunch ();
}
catch (const std::string& message)
@ -233,6 +232,12 @@ int Context::initialize (int argc, const char** argv)
rc = 2;
}
catch (int)
{
// Hooks can terminate processing by throwing integers.
rc = 4;
}
catch (...)
{
error (STRING_UNKNOWN_ERROR);
@ -297,6 +302,7 @@ int Context::run ()
try
{
rc = dispatch (output);
hooks.onExit ();
std::stringstream s;
s << "Perf "
@ -317,13 +323,15 @@ int Context::run ()
<< " commit:" << timer_commit.total ()
<< " sort:" << timer_sort.total ()
<< " render:" << timer_render.total ()
<< " hooks:" << timer_hooks.total ()
<< " total:" << (timer_init.total () +
timer_load.total () +
timer_gc.total () +
timer_filter.total () +
timer_commit.total () +
timer_sort.total () +
timer_render.total ())
timer_render.total () +
timer_hooks.total ())
<< "\n";
debug (s.str ());
}
@ -385,7 +393,6 @@ int Context::run ()
else
std::cerr << *e << "\n";
hooks.trigger ("on-exit");
return rc;
}

View file

@ -120,6 +120,7 @@ public:
Timer timer_commit;
Timer timer_sort;
Timer timer_render;
Timer timer_hooks;
};
#endif

View file

@ -24,69 +24,19 @@
//
////////////////////////////////////////////////////////////////////////////////
#include <iostream> // TODO Remove
#include <cmake.h>
#include <iostream>
#include <algorithm>
#include <stdio.h>
#include <Context.h>
#include <Hooks.h>
#include <Timer.h>
#include <text.h>
#include <i18n.h>
extern Context context;
////////////////////////////////////////////////////////////////////////////////
Hook::Hook ()
: _event ("")
, _file ("")
, _function ("")
{
}
////////////////////////////////////////////////////////////////////////////////
Hook::Hook (const std::string& e, const std::string& f, const std::string& fn)
: _event (e)
, _file (f)
, _function (fn)
{
}
////////////////////////////////////////////////////////////////////////////////
Hook::Hook (const Hook& other)
{
_event = other._event;
_file = other._file;
_function = other._function;
}
////////////////////////////////////////////////////////////////////////////////
Hook& Hook::operator= (const Hook& other)
{
if (this != &other)
{
_event = other._event;
_file = other._file;
_function = other._function;
}
return *this;
}
////////////////////////////////////////////////////////////////////////////////
Hooks::Hooks ()
{
// New 2.x hooks.
_validTaskEvents.push_back ("on-task-add"); // Unimplemented
_validTaskEvents.push_back ("on-task-modify"); // Unimplemented
_validTaskEvents.push_back ("on-task-complete"); // Unimplemented
_validTaskEvents.push_back ("on-task-delete"); // Unimplemented
_validProgramEvents.push_back ("on-launch");
_validProgramEvents.push_back ("on-exit");
_validProgramEvents.push_back ("on-file-read"); // Unimplemented
_validProgramEvents.push_back ("on-file-write"); // Unimplemented
_validProgramEvents.push_back ("on-synch"); // Unimplemented
_validProgramEvents.push_back ("on-gc"); // Unimplemented
}
////////////////////////////////////////////////////////////////////////////////
@ -95,86 +45,285 @@ Hooks::~Hooks ()
}
////////////////////////////////////////////////////////////////////////////////
// Enumerate all hooks, and tell API about the script files it must load in
// order to call them. Note that API will perform a deferred read, which means
// that if it isn't called, a script will not be loaded.
void Hooks::initialize ()
{
// Allow a master switch to turn the whole thing off.
bool big_red_switch = context.config.getBoolean ("extensions");
if (big_red_switch)
// Scan <rc.data.location>/hooks
Directory d (context.config.get ("data.location"));
d += "hooks";
if (d.is_directory () &&
d.readable ())
{
Config::const_iterator it;
for (it = context.config.begin (); it != context.config.end (); ++it)
{
std::string type;
std::string name;
std::string value;
_scripts = d.list ();
std::sort (_scripts.begin (), _scripts.end ());
}
}
// "<type>.<name>"
Nibbler n (it->first);
if (n.getUntil ('.', type) &&
type == "hook" &&
n.skip ('.') &&
n.getUntilEOS (name))
{
Nibbler n (it->second);
////////////////////////////////////////////////////////////////////////////////
// Occurs when: On launch, after data structures are initiliazed, before
// data is loaded.
// Data fed to stdin: None
// Exit code: 0: Success, proceed
// !0: Failure, terminate
// Output handled: 0: context.header ()
// !0: context.error ()
void Hooks::onLaunch ()
{
context.timer_hooks.start ();
// <path>:<function> [, ...]
while (!n.depleted ())
std::vector <std::string> matchingScripts = scripts ("on-launch");
std::vector <std::string>::iterator i;
for (i = matchingScripts.begin (); i != matchingScripts.end (); ++i)
{
std::string file;
std::string function;
if (n.getUntil (':', file) &&
n.skip (':') &&
n.getUntil (',', function))
{
context.debug (std::string ("Event '") + name + "' hooked by " + file + ", function " + function);
Hook h (name, Path::expand (file), function);
_all.push_back (h);
std::string output;
int status = execute (*i, "", output);
(void) n.skip (',');
std::vector <std::string> lines;
split (lines, output, '\n');
std::vector <std::string>::iterator line;
if (status == 0)
{
for (line = lines.begin (); line != lines.end (); ++line)
{
if (line->length () && (*line)[0] == '{')
{
Task newTask (*line);
context.tdb2.add (newTask);
}
else
; // Was: throw std::string (format ("Malformed hook definition '{1}'.", it->first));
}
}
context.header (*line);
}
}
else
context.debug ("Hooks::initialize --> off");
{
for (line = lines.begin (); line != lines.end (); ++line)
context.error (*line);
throw 0; // This is how hooks silently terminate processing.
}
}
context.timer_hooks.stop ();
}
////////////////////////////////////////////////////////////////////////////////
// Program hooks.
bool Hooks::trigger (const std::string& event)
// Occurs when: On exit, after processing is complete, before output is
// displayed.
// Data fed to stdin: None
// Exit code: 0: Success
// !0: Failure
// Output handled: 0: context.footnote ()
// !0: context.error ()
void Hooks::onExit ()
{
return false;
context.timer_hooks.start ();
std::vector <std::string> matchingScripts = scripts ("on-exit");
std::vector <std::string>::iterator i;
for (i = matchingScripts.begin (); i != matchingScripts.end (); ++i)
{
std::string output;
int status = execute (*i, "", output);
std::vector <std::string> lines;
split (lines, output, '\n');
std::vector <std::string>::iterator line;
if (status == 0)
{
for (line = lines.begin (); line != lines.end (); ++line)
{
if (line->length () && (*line)[0] == '{')
{
Task newTask (*line);
context.tdb2.add (newTask);
}
else
context.footnote (*line);
}
}
else
{
for (line = lines.begin (); line != lines.end (); ++line)
context.error (*line);
}
}
context.timer_hooks.stop ();
}
////////////////////////////////////////////////////////////////////////////////
// Task hooks.
bool Hooks::trigger (const std::string& event, Task& task)
// Occurs when: A task is created, before it is committed.
// Data fed to stdin: task JSON
// Exit code: 0: Success
// !0: Failure
// Output handled: 0: modified JSON
// context.footnote ()
// !0: context.error ()
void Hooks::onAdd (Task& after)
{
return false;
context.timer_hooks.start ();
std::vector <std::string> matchingScripts = scripts ("on-add");
std::vector <std::string>::iterator i;
for (i = matchingScripts.begin (); i != matchingScripts.end (); ++i)
{
std::string input = after.composeJSON () + "\n";
std::string output;
int status = execute (*i, input, output);
std::vector <std::string> lines;
split (lines, output, '\n');
std::vector <std::string>::iterator line;
if (status == 0)
{
bool first = true;
for (line = lines.begin (); line != lines.end (); ++line)
{
if (line->length () && (*line)[0] == '{')
{
Task newTask (*line);
if (first)
{
after = newTask;
first = false;
}
else
context.tdb2.add (newTask);
}
else
context.footnote (*line);
}
}
else
{
for (line = lines.begin (); line != lines.end (); ++line)
context.error (*line);
throw 0; // This is how hooks silently terminate processing.
}
}
context.timer_hooks.stop ();
}
////////////////////////////////////////////////////////////////////////////////
bool Hooks::validProgramEvent (const std::string& event)
// Occurs when: A task is modified, before it is committed.
// Data fed to stdin: before JSON
// after JSON
// Exit code: 0: Success
// !0: Failure
// Output handled: 0: modified after JSON
// context.footnote ()
// !0: context.error ()
void Hooks::onModify (const Task& before, Task& after)
{
if (std::find (_validProgramEvents.begin (), _validProgramEvents.end (), event) != _validProgramEvents.end ())
return true;
context.timer_hooks.start ();
return false;
std::vector <std::string> matchingScripts = scripts ("on-modify");
std::vector <std::string>::iterator i;
for (i = matchingScripts.begin (); i != matchingScripts.end (); ++i)
{
std::string afterJSON = after.composeJSON ();
std::string input = before.composeJSON ()
+ "\n"
+ afterJSON
+ "\n";
std::string output;
int status = execute (*i, input, output);
std::vector <std::string> lines;
split (lines, output, '\n');
std::vector <std::string>::iterator line;
if (status == 0)
{
bool first = true;
for (line = lines.begin (); line != lines.end (); ++line)
{
if (line->length () && (*line)[0] == '{')
{
Task newTask (*line);
if (first)
{
after = newTask;
first = false;
}
else
context.tdb2.add (newTask);
}
else
context.footnote (*line);
}
}
else
{
for (line = lines.begin (); line != lines.end (); ++line)
context.error (*line);
throw 0; // This is how hooks silently terminate processing.
}
}
context.timer_hooks.stop ();
}
////////////////////////////////////////////////////////////////////////////////
bool Hooks::validTaskEvent (const std::string& event)
std::vector <std::string> Hooks::scripts (const std::string& event)
{
if (std::find (_validTaskEvents.begin (), _validTaskEvents.end (), event) != _validTaskEvents.end ())
return true;
std::vector <std::string> matching;
std::vector <std::string>::iterator i;
for (i = _scripts.begin (); i != _scripts.end (); ++i)
{
if (i->find ("/" + event) != std::string::npos)
{
File script (*i);
if (script.executable ())
matching.push_back (*i);
}
}
return false;
return matching;
}
////////////////////////////////////////////////////////////////////////////////
int Hooks::execute (
const std::string& command,
const std::string& input,
std::string& output)
{
int status = -1;
FILE* fp = popen (command.c_str (), "r+");
if (fp)
{
// Write input to fp.
if (input != "" &&
input != "\n")
{
fputs (input.c_str (), fp);
fflush (fp);
}
// Read output from fp.
output = "";
char* line = NULL;
size_t len = 0;
while (getline (&line, &len, fp) != -1)
{
output += line;
free (line);
line = NULL;
}
fflush (fp);
status = pclose (fp);
context.debug (format ("Hooks::execute {1} (status {2})", command, status));
}
return status;
}
////////////////////////////////////////////////////////////////////////////////

View file

@ -30,22 +30,6 @@
#include <vector>
#include <string>
// Hook class representing a single hook, which is just a three-way map.
class Hook
{
public:
Hook ();
Hook (const std::string&, const std::string&, const std::string&);
Hook (const Hook&);
Hook& operator= (const Hook&);
public:
std::string _event;
std::string _file;
std::string _function;
};
// Hooks class for managing the loading and calling of hook functions.
class Hooks
{
public:
@ -56,18 +40,17 @@ public:
void initialize ();
bool trigger (const std::string&); // Program
bool trigger (const std::string&, Task&); // Task
void onLaunch ();
void onExit ();
void onAdd (Task&);
void onModify (const Task&, Task&);
private:
bool validProgramEvent (const std::string&);
bool validTaskEvent (const std::string&);
std::vector <std::string> scripts (const std::string&);
int execute (const std::string&, const std::string&, std::string&);
private:
std::vector <Hook> _all; // All current hooks.
std::vector <std::string> _validProgramEvents;
std::vector <std::string> _validTaskEvents;
std::vector <std::string> _scripts;
};
#endif

View file

@ -134,10 +134,13 @@ bool TF2::get (const std::string& uuid, Task& task)
////////////////////////////////////////////////////////////////////////////////
void TF2::add_task (const Task& task)
{
_tasks.push_back (task); // For subsequent queries
_added_tasks.push_back (task); // For commit/synch
Task hookTask (task);
context.hooks.onAdd (hookTask);
/* TODO handle 'add' and 'log'.
_tasks.push_back (hookTask); // For subsequent queries
_added_tasks.push_back (hookTask); // For commit/synch
/* TODO handle 'add' and 'log'?
int id = context.tdb2.next_id ();
_I2U[id] = task.get ("uuid");
_U2I[task.get ("uuid")] = id;
@ -156,9 +159,13 @@ bool TF2::modify_task (const Task& task)
{
if (i->get ("uuid") == uuid)
{
*i = task;
_modified_tasks.push_back (task);
Task hookTask (task);
context.hooks.onModify (*i, hookTask);
*i = hookTask;
_modified_tasks.push_back (hookTask);
_dirty = true;
return true;
}
}