Skip to content

Commit

Permalink
Implemented trigger file support
Browse files Browse the repository at this point in the history
  • Loading branch information
0xeb committed Jul 7, 2021
1 parent d4f692d commit 27e50d1
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 37 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
__pycache__/
build/
build64/
.vs/
Expand Down
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,27 @@ It is possible to execute a script from `QScripts` without having to activate it

It is possible to instruct `QScripts` to re-execute the active script if any of its dependent scripts are also modified. To use the automatic dependency system, please create a file named exactly like your active script but with the additional `.deps.qscripts` extension. In that file you put your dependent scripts full path.

When using Python, it would be helpful if we can also `reload` the changed dependent script from the active script automatically. To do that, simply add the directive line `/reload` along with the desired reload syntax. For example, here's a complete `.deps.qscripts` file with a `reload` directive:
When using Python, it would be helpful if we can also `reload` the changed dependent script from the active script automatically. To do that, simply add the directive line `/reload` along with the desired reload syntax. For example, here's a complete `.deps.qscripts` file with a `reload` directive (for Python 2.x):

```
/reload reload($basename$)
t2.py
//This is a comment
// This is a comment
t3.py
```

And for Python 3.x:

```
/reload import imp;imp.reload($basename$);
t2.py
// This is a comment
t3.py
```

So what happens now if we have an active file `t1.py` with the dependency file above?

1. Any time `t1.py` changes, it will be automatically re-executed in IDA. That's the default behavior of `QScripts` <= 1.0.5.
1. Any time `t1.py` changes, it will be automatically re-executed in IDA.
2. If the dependency index file `t1.py.deps.qscripts` is changed, then your new dependencies will be reloaded and the active script will be executed again.
3. If any dependency script file has changed, then the active script will re-execute. If you had a `reload` directive set up, then the modified dependency files will also be reloaded.

Expand All @@ -54,6 +63,21 @@ Please note that if each dependent script file has its own dependency index file
* `$env:EnvVariableName$`: `EnvVariableName` is expanded to its environment variable value if it exists or left unexpanded otherwise


## Using QScripts with trigger files

Sometimes you don't want to trigger QScripts when your working scripts are saved, instead you want your own trigger condition.
One way to achieve a custom trigger is by using the `/triggerfile` directive:

```
/reload import imp;imp.reload($basename$);
/triggerfile createme.tmp
// Just some dependencies:
dep.py
```

This tells QScripts to wait until the trigger file `createme.tmp` is created before executing your script. Now, any time you want to invoke QScripts, just create the trigger file. The moment QScripts finds the trigger file, it deletes it and then always executes your active script (and reloads dependencies when applicable).

## Using QScripts programmatically
It is possible to invoke `QScripts` from a script. For instance, in IDAPython, you can execute the last selected script with:

Expand Down
115 changes: 84 additions & 31 deletions qscripts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,23 @@ struct fileinfo_t
{
std::string file_path;
qtime64_t modified_time;
bool operator==(const fileinfo_t &rhs) const
{
return file_path == rhs.file_path;
}

fileinfo_t(const char *script_file = nullptr)
fileinfo_t(const char* script_file = nullptr)
{
if (script_file != nullptr)
this->file_path = script_file;
}

const bool empty() const
{
return file_path.empty();
}

bool operator==(const fileinfo_t &rhs) const
{
return file_path == rhs.file_path;
}

virtual void clear()
{
file_path.clear();
Expand All @@ -71,7 +77,7 @@ struct fileinfo_t
{
qtime64_t cur_mtime;
const char *script_file = this->file_path.c_str();
if (!get_file_modification_time(script_file, cur_mtime))
if (!get_file_modification_time(script_file, &cur_mtime))
return -1;

// Script is up to date, no need to execute it again
Expand All @@ -80,6 +86,7 @@ struct fileinfo_t

if (update_mtime)
modified_time = cur_mtime;

return 1;
}
};
Expand All @@ -101,6 +108,9 @@ struct dep_script_info_t: fileinfo_t
// Active script information along with its dependencies
struct active_script_info_t: script_info_t
{
// Trigger file name
qstring trigger_file;

// The dependencies index files. First entry is for the main script's deps
qvector<fileinfo_t> dep_indices;

Expand All @@ -113,7 +123,10 @@ struct active_script_info_t: script_info_t
return dep_scripts.find(dep_file) != dep_scripts.end();
}

// If no dependency index files have been modified, we return 0
// Is this trigger based or dependency based?
const bool trigger_based() { return !trigger_file.empty(); }

// If no dependency index files have been modified, return 0.
// Return 1 if one of them has been modified or -1 if one of them has gone missing.
// In both latter cases, we have to recompute our dependencies
int is_any_dep_index_modified(bool update_mtime = true)
Expand All @@ -131,7 +144,7 @@ struct active_script_info_t: script_info_t
bool add_dep_index(const char *dep_file)
{
fileinfo_t fi;
if (!get_file_modification_time(dep_file, fi.modified_time))
if (!get_file_modification_time(dep_file, &fi.modified_time))
return false;

fi.file_path = dep_file;
Expand All @@ -156,11 +169,18 @@ struct active_script_info_t: script_info_t
script_info_t::clear();
dep_indices.qclear();
dep_scripts.clear();
trigger_file.clear();
}

void invalidate_all_scripts()
void invalidate()
{
modified_time = 0;
}

void invalidate_all_scripts()
{
invalidate();

// Invalidate all but the index file itself
for (auto &kv: dep_scripts)
kv.second.modified_time = 0;
Expand Down Expand Up @@ -208,6 +228,7 @@ struct qscripts_chooser_t: public chooser_t
return false;
}

// Add the dependency file to the active script
selected_script.add_dep_index(dep_file.c_str());

qstring reload_cmd;
Expand All @@ -228,31 +249,24 @@ struct qscripts_chooser_t: public chooser_t
reload_cmd = line.c_str() + 8;
continue;
}
else if (strncmp(line.c_str(), "/triggerfile ", 13) == 0)
{
selected_script.trigger_file = line.c_str() + 13;
expand_file_name(selected_script.trigger_file, script_file);
continue;
}
}

// From here on, any other line is an expandable string leading to a script file
expand_string(line, line, script_file);

if (!qisabspath(line.c_str()))
{
qstring dir_name = script_file;
qdirname(dir_name.begin(), dir_name.size(), script_file);

qstring full_path;
full_path.sprnt("%s" SDIRCHAR "%s", dir_name.c_str(), line.c_str());
line = full_path;
}

// Always normalize the final script path
normalize_path_sep(line);
expand_file_name(line, script_file);

// Skip dependency scripts that (do not|no longer) exist
dep_script_info_t dep_script;
if (!get_file_modification_time(line.c_str(), dep_script.modified_time))
if (!get_file_modification_time(line, &dep_script.modified_time))
continue;

// Add script
dep_script.file_path = line.c_str();
dep_script.file_path = line.c_str();
dep_script.reload_cmd = reload_cmd;
selected_script.dep_scripts[line.c_str()] = std::move(dep_script);

Expand All @@ -263,6 +277,24 @@ struct qscripts_chooser_t: public chooser_t
return true;
}

void expand_file_name(qstring &filename, const char *templ_file)
{
expand_string(filename, filename, templ_file);

if (!qisabspath(filename.c_str()))
{
qstring dir_name = templ_file;
qdirname(dir_name.begin(), dir_name.size(), templ_file);

qstring full_path;
full_path.sprnt("%s" SDIRCHAR "%s", dir_name.c_str(), filename.c_str());
filename = full_path;
}

// Always normalize the final script path
normalize_path_sep(filename);
}

void set_selected_script(script_info_t &script)
{
// Activate script
Expand All @@ -286,6 +318,11 @@ struct qscripts_chooser_t: public chooser_t

bool is_monitor_active() const { return m_b_filemon_timer_active; }

bool is_using_trigger_file()
{
return has_selected_script() && !selected_script.trigger_file.empty();
}

void expand_string(qstring &input, qstring &output, const char *script_file)
{
output = std::regex_replace(
Expand Down Expand Up @@ -359,7 +396,7 @@ struct qscripts_chooser_t: public chooser_t
auto script_file = script_info->file_path.c_str();

// First things first: always take the file's modification timestamp first so not to visit it again in the file monitor timer
if (!get_file_modification_time(script_file, script_info->modified_time))
if (!get_file_modification_time(script_file, &script_info->modified_time))
{
msg("Script file '%s' not found!\n", script_file);
break;
Expand Down Expand Up @@ -487,6 +524,7 @@ struct qscripts_chooser_t: public chooser_t
return ((qscripts_chooser_t *)ud)->filemon_timer_cb();
}

// Monitor callback
int filemon_timer_cb()
{
do
Expand All @@ -495,6 +533,21 @@ struct qscripts_chooser_t: public chooser_t
if (!is_monitor_active() || !has_selected_script())
break;

// In trigger file mode, just wait for the trigger file to be created
if (selected_script.trigger_based())
{
// The monitor waits until the trigger file is created
auto trigger_file = selected_script.trigger_file.c_str();
if (!get_file_modification_time(trigger_file))
break;

// Delete trigger file
qunlink(trigger_file);
// Always execute the main script even if it was not changed
selected_script.invalidate();
// ...and proceed with qscript logic
}

// Check if the active script or its dependencies are changed:
// 1. Dependency file --> repopulate it and execute active script
// 2. Any dependencies --> reload if needed and //
Expand Down Expand Up @@ -570,11 +623,11 @@ struct qscripts_chooser_t: public chooser_t

static int widths_[2];
static const char *const header_[2];

static char ACTION_DEACTIVATE_MONITOR_ID[];
static char ACTION_EXECUTE_SELECTED_SCRIPT_ID[];
static char ACTION_EXECUTE_SCRIPT_WITH_UNDO_ID[];

scripts_info_t m_scripts;
ssize_t m_nselected = NO_SELECTION;

Expand Down Expand Up @@ -608,7 +661,7 @@ struct qscripts_chooser_t: public chooser_t

virtual int idaapi activate(action_activation_ctx_t *ctx) override
{
ch->activate_monitor(false);
ch->clear_selected_script();
refresh_chooser(QSCRIPTS_TITLE);
return 1;
}
Expand Down Expand Up @@ -691,7 +744,7 @@ struct qscripts_chooser_t: public chooser_t
}

qtime64_t mtime;
if (!get_file_modification_time(script_file, mtime))
if (!get_file_modification_time(script_file, &mtime))
{
if (!silent)
msg("Script file not found: '%s'\n", script_file);
Expand Down Expand Up @@ -948,7 +1001,7 @@ struct qscripts_chooser_t: public chooser_t
void execute_last_selected_script(bool with_undo=false)
{
if (has_selected_script())
execute_script(&selected_script, with_undo);
execute_script(&selected_script, with_undo);
}

void execute_script_at(ssize_t n)
Expand Down
6 changes: 6 additions & 0 deletions test_scripts/trigger-file/dep.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
try:
trigger_ver += 1
except:
trigger_ver = 1

print(f"dep file version {trigger_ver}: Even if you save this file, this script won't re-execute. Instead, create the trigger file.")
11 changes: 11 additions & 0 deletions test_scripts/trigger-file/trigger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import idaapi
import dep

try:
trigger_ver += 1
except:
trigger_ver = 1

print(f"version {trigger_ver}: Even if you save this file, this script won't re-execute. Instead, create the trigger file.")

print("----------------------")
3 changes: 3 additions & 0 deletions test_scripts/trigger-file/trigger.py.deps.qscripts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/reload import imp;imp.reload($basename$);
/triggerfile createme.tmp
dep.py
15 changes: 12 additions & 3 deletions utils_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,22 @@ struct collect_extlangs: extlang_visitor_t
// Utility function to return a file's last modification timestamp
bool get_file_modification_time(
const char *filename,
qtime64_t &mtime)
qtime64_t *mtime = nullptr)
{
qstatbuf stat_buf;
if (qstat(filename, &stat_buf) != 0)
return false;
else
return mtime = stat_buf.qst_mtime, true;

if (mtime != nullptr)
*mtime = stat_buf.qst_mtime;
return true;
}

bool get_file_modification_time(
const qstring &filename,
qtime64_t *mtime = nullptr)
{
return get_file_modification_time(filename.c_str(), mtime);
}

//-------------------------------------------------------------------------
Expand Down

0 comments on commit 27e50d1

Please sign in to comment.