diff --git a/scripts/CMakeLists.txt b/scripts/CMakeLists.txt index 9de02dc8c..edc8e2eb5 100644 --- a/scripts/CMakeLists.txt +++ b/scripts/CMakeLists.txt @@ -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 diff --git a/scripts/hooks/on-add b/scripts/hooks/on-add new file mode 100755 index 000000000..3c0ce92e1 --- /dev/null +++ b/scripts/hooks/on-add @@ -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 + diff --git a/scripts/hooks/on-exit b/scripts/hooks/on-exit new file mode 100755 index 000000000..7a7db78d5 --- /dev/null +++ b/scripts/hooks/on-exit @@ -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 + diff --git a/scripts/hooks/on-launch b/scripts/hooks/on-launch new file mode 100755 index 000000000..ed5ccf94a --- /dev/null +++ b/scripts/hooks/on-launch @@ -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 + diff --git a/scripts/hooks/on-modify b/scripts/hooks/on-modify new file mode 100755 index 000000000..8107f93ab --- /dev/null +++ b/scripts/hooks/on-modify @@ -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 + diff --git a/src/Config.cpp b/src/Config.cpp index 390522203..131494fe7 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -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 (); } } diff --git a/src/Context.cpp b/src/Context.cpp index 3a9f48211..6cc592bea 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -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; } diff --git a/src/Context.h b/src/Context.h index 905b7f187..28af481ec 100644 --- a/src/Context.h +++ b/src/Context.h @@ -120,6 +120,7 @@ public: Timer timer_commit; Timer timer_sort; Timer timer_render; + Timer timer_hooks; }; #endif diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 55eda25e5..c99ab6425 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -24,69 +24,19 @@ // //////////////////////////////////////////////////////////////////////////////// +#include // TODO Remove #include -#include #include +#include #include #include -#include #include -#include 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 /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) + _scripts = d.list (); + std::sort (_scripts.begin (), _scripts.end ()); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// 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 (); + + std::vector matchingScripts = scripts ("on-launch"); + std::vector ::iterator i; + for (i = matchingScripts.begin (); i != matchingScripts.end (); ++i) + { + std::string output; + int status = execute (*i, "", output); + + std::vector lines; + split (lines, output, '\n'); + std::vector ::iterator line; + + if (status == 0) { - std::string type; - std::string name; - std::string value; - - // "." - Nibbler n (it->first); - if (n.getUntil ('.', type) && - type == "hook" && - n.skip ('.') && - n.getUntilEOS (name)) + for (line = lines.begin (); line != lines.end (); ++line) { - Nibbler n (it->second); - - // : [, ...] - while (!n.depleted ()) + if (line->length () && (*line)[0] == '{') { - 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); - - (void) n.skip (','); - } - else - ; // Was: throw std::string (format ("Malformed hook definition '{1}'.", it->first)); + Task newTask (*line); + context.tdb2.add (newTask); } + else + context.header (*line); } } + else + { + for (line = lines.begin (); line != lines.end (); ++line) + context.error (*line); + + throw 0; // This is how hooks silently terminate processing. + } } - else - context.debug ("Hooks::initialize --> off"); + + 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 matchingScripts = scripts ("on-exit"); + std::vector ::iterator i; + for (i = matchingScripts.begin (); i != matchingScripts.end (); ++i) + { + std::string output; + int status = execute (*i, "", output); + + std::vector lines; + split (lines, output, '\n'); + std::vector ::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 matchingScripts = scripts ("on-add"); + std::vector ::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 lines; + split (lines, output, '\n'); + std::vector ::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 matchingScripts = scripts ("on-modify"); + std::vector ::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 lines; + split (lines, output, '\n'); + std::vector ::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 Hooks::scripts (const std::string& event) { - if (std::find (_validTaskEvents.begin (), _validTaskEvents.end (), event) != _validTaskEvents.end ()) - return true; + std::vector matching; + std::vector ::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; } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/Hooks.h b/src/Hooks.h index 02b67adec..d6501da84 100644 --- a/src/Hooks.h +++ b/src/Hooks.h @@ -30,22 +30,6 @@ #include #include -// 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 scripts (const std::string&); + int execute (const std::string&, const std::string&, std::string&); private: - std::vector _all; // All current hooks. - - std::vector _validProgramEvents; - std::vector _validTaskEvents; + std::vector _scripts; }; #endif diff --git a/src/TDB2.cpp b/src/TDB2.cpp index 475e86568..a6428d250 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -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; } }