Skip to content

Commit 27e50d1

Browse files
committed
Implemented trigger file support
1 parent d4f692d commit 27e50d1

File tree

7 files changed

+144
-37
lines changed

7 files changed

+144
-37
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
__pycache__/
12
build/
23
build64/
34
.vs/

README.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,27 @@ It is possible to execute a script from `QScripts` without having to activate it
3131

3232
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.
3333

34-
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:
34+
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):
3535

3636
```
3737
/reload reload($basename$)
3838
t2.py
39-
//This is a comment
39+
// This is a comment
40+
t3.py
41+
```
42+
43+
And for Python 3.x:
44+
45+
```
46+
/reload import imp;imp.reload($basename$);
47+
t2.py
48+
// This is a comment
4049
t3.py
4150
```
4251

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

45-
1. Any time `t1.py` changes, it will be automatically re-executed in IDA. That's the default behavior of `QScripts` <= 1.0.5.
54+
1. Any time `t1.py` changes, it will be automatically re-executed in IDA.
4655
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.
4756
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.
4857

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

5665

66+
## Using QScripts with trigger files
67+
68+
Sometimes you don't want to trigger QScripts when your working scripts are saved, instead you want your own trigger condition.
69+
One way to achieve a custom trigger is by using the `/triggerfile` directive:
70+
71+
```
72+
/reload import imp;imp.reload($basename$);
73+
/triggerfile createme.tmp
74+
75+
// Just some dependencies:
76+
dep.py
77+
```
78+
79+
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).
80+
5781
## Using QScripts programmatically
5882
It is possible to invoke `QScripts` from a script. For instance, in IDAPython, you can execute the last selected script with:
5983

qscripts.cpp

Lines changed: 84 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,23 @@ struct fileinfo_t
4444
{
4545
std::string file_path;
4646
qtime64_t modified_time;
47-
bool operator==(const fileinfo_t &rhs) const
48-
{
49-
return file_path == rhs.file_path;
50-
}
5147

52-
fileinfo_t(const char *script_file = nullptr)
48+
fileinfo_t(const char* script_file = nullptr)
5349
{
5450
if (script_file != nullptr)
5551
this->file_path = script_file;
5652
}
5753

54+
const bool empty() const
55+
{
56+
return file_path.empty();
57+
}
58+
59+
bool operator==(const fileinfo_t &rhs) const
60+
{
61+
return file_path == rhs.file_path;
62+
}
63+
5864
virtual void clear()
5965
{
6066
file_path.clear();
@@ -71,7 +77,7 @@ struct fileinfo_t
7177
{
7278
qtime64_t cur_mtime;
7379
const char *script_file = this->file_path.c_str();
74-
if (!get_file_modification_time(script_file, cur_mtime))
80+
if (!get_file_modification_time(script_file, &cur_mtime))
7581
return -1;
7682

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

8187
if (update_mtime)
8288
modified_time = cur_mtime;
89+
8390
return 1;
8491
}
8592
};
@@ -101,6 +108,9 @@ struct dep_script_info_t: fileinfo_t
101108
// Active script information along with its dependencies
102109
struct active_script_info_t: script_info_t
103110
{
111+
// Trigger file name
112+
qstring trigger_file;
113+
104114
// The dependencies index files. First entry is for the main script's deps
105115
qvector<fileinfo_t> dep_indices;
106116

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

116-
// If no dependency index files have been modified, we return 0
126+
// Is this trigger based or dependency based?
127+
const bool trigger_based() { return !trigger_file.empty(); }
128+
129+
// If no dependency index files have been modified, return 0.
117130
// Return 1 if one of them has been modified or -1 if one of them has gone missing.
118131
// In both latter cases, we have to recompute our dependencies
119132
int is_any_dep_index_modified(bool update_mtime = true)
@@ -131,7 +144,7 @@ struct active_script_info_t: script_info_t
131144
bool add_dep_index(const char *dep_file)
132145
{
133146
fileinfo_t fi;
134-
if (!get_file_modification_time(dep_file, fi.modified_time))
147+
if (!get_file_modification_time(dep_file, &fi.modified_time))
135148
return false;
136149

137150
fi.file_path = dep_file;
@@ -156,11 +169,18 @@ struct active_script_info_t: script_info_t
156169
script_info_t::clear();
157170
dep_indices.qclear();
158171
dep_scripts.clear();
172+
trigger_file.clear();
159173
}
160174

161-
void invalidate_all_scripts()
175+
void invalidate()
162176
{
163177
modified_time = 0;
178+
}
179+
180+
void invalidate_all_scripts()
181+
{
182+
invalidate();
183+
164184
// Invalidate all but the index file itself
165185
for (auto &kv: dep_scripts)
166186
kv.second.modified_time = 0;
@@ -208,6 +228,7 @@ struct qscripts_chooser_t: public chooser_t
208228
return false;
209229
}
210230

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

213234
qstring reload_cmd;
@@ -228,31 +249,24 @@ struct qscripts_chooser_t: public chooser_t
228249
reload_cmd = line.c_str() + 8;
229250
continue;
230251
}
252+
else if (strncmp(line.c_str(), "/triggerfile ", 13) == 0)
253+
{
254+
selected_script.trigger_file = line.c_str() + 13;
255+
expand_file_name(selected_script.trigger_file, script_file);
256+
continue;
257+
}
231258
}
232259

233260
// From here on, any other line is an expandable string leading to a script file
234-
expand_string(line, line, script_file);
235-
236-
if (!qisabspath(line.c_str()))
237-
{
238-
qstring dir_name = script_file;
239-
qdirname(dir_name.begin(), dir_name.size(), script_file);
240-
241-
qstring full_path;
242-
full_path.sprnt("%s" SDIRCHAR "%s", dir_name.c_str(), line.c_str());
243-
line = full_path;
244-
}
245-
246-
// Always normalize the final script path
247-
normalize_path_sep(line);
261+
expand_file_name(line, script_file);
248262

249263
// Skip dependency scripts that (do not|no longer) exist
250264
dep_script_info_t dep_script;
251-
if (!get_file_modification_time(line.c_str(), dep_script.modified_time))
265+
if (!get_file_modification_time(line, &dep_script.modified_time))
252266
continue;
253267

254268
// Add script
255-
dep_script.file_path = line.c_str();
269+
dep_script.file_path = line.c_str();
256270
dep_script.reload_cmd = reload_cmd;
257271
selected_script.dep_scripts[line.c_str()] = std::move(dep_script);
258272

@@ -263,6 +277,24 @@ struct qscripts_chooser_t: public chooser_t
263277
return true;
264278
}
265279

280+
void expand_file_name(qstring &filename, const char *templ_file)
281+
{
282+
expand_string(filename, filename, templ_file);
283+
284+
if (!qisabspath(filename.c_str()))
285+
{
286+
qstring dir_name = templ_file;
287+
qdirname(dir_name.begin(), dir_name.size(), templ_file);
288+
289+
qstring full_path;
290+
full_path.sprnt("%s" SDIRCHAR "%s", dir_name.c_str(), filename.c_str());
291+
filename = full_path;
292+
}
293+
294+
// Always normalize the final script path
295+
normalize_path_sep(filename);
296+
}
297+
266298
void set_selected_script(script_info_t &script)
267299
{
268300
// Activate script
@@ -286,6 +318,11 @@ struct qscripts_chooser_t: public chooser_t
286318

287319
bool is_monitor_active() const { return m_b_filemon_timer_active; }
288320

321+
bool is_using_trigger_file()
322+
{
323+
return has_selected_script() && !selected_script.trigger_file.empty();
324+
}
325+
289326
void expand_string(qstring &input, qstring &output, const char *script_file)
290327
{
291328
output = std::regex_replace(
@@ -359,7 +396,7 @@ struct qscripts_chooser_t: public chooser_t
359396
auto script_file = script_info->file_path.c_str();
360397

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

527+
// Monitor callback
490528
int filemon_timer_cb()
491529
{
492530
do
@@ -495,6 +533,21 @@ struct qscripts_chooser_t: public chooser_t
495533
if (!is_monitor_active() || !has_selected_script())
496534
break;
497535

536+
// In trigger file mode, just wait for the trigger file to be created
537+
if (selected_script.trigger_based())
538+
{
539+
// The monitor waits until the trigger file is created
540+
auto trigger_file = selected_script.trigger_file.c_str();
541+
if (!get_file_modification_time(trigger_file))
542+
break;
543+
544+
// Delete trigger file
545+
qunlink(trigger_file);
546+
// Always execute the main script even if it was not changed
547+
selected_script.invalidate();
548+
// ...and proceed with qscript logic
549+
}
550+
498551
// Check if the active script or its dependencies are changed:
499552
// 1. Dependency file --> repopulate it and execute active script
500553
// 2. Any dependencies --> reload if needed and //
@@ -570,11 +623,11 @@ struct qscripts_chooser_t: public chooser_t
570623

571624
static int widths_[2];
572625
static const char *const header_[2];
573-
626+
574627
static char ACTION_DEACTIVATE_MONITOR_ID[];
575628
static char ACTION_EXECUTE_SELECTED_SCRIPT_ID[];
576629
static char ACTION_EXECUTE_SCRIPT_WITH_UNDO_ID[];
577-
630+
578631
scripts_info_t m_scripts;
579632
ssize_t m_nselected = NO_SELECTION;
580633

@@ -608,7 +661,7 @@ struct qscripts_chooser_t: public chooser_t
608661

609662
virtual int idaapi activate(action_activation_ctx_t *ctx) override
610663
{
611-
ch->activate_monitor(false);
664+
ch->clear_selected_script();
612665
refresh_chooser(QSCRIPTS_TITLE);
613666
return 1;
614667
}
@@ -691,7 +744,7 @@ struct qscripts_chooser_t: public chooser_t
691744
}
692745

693746
qtime64_t mtime;
694-
if (!get_file_modification_time(script_file, mtime))
747+
if (!get_file_modification_time(script_file, &mtime))
695748
{
696749
if (!silent)
697750
msg("Script file not found: '%s'\n", script_file);
@@ -948,7 +1001,7 @@ struct qscripts_chooser_t: public chooser_t
9481001
void execute_last_selected_script(bool with_undo=false)
9491002
{
9501003
if (has_selected_script())
951-
execute_script(&selected_script, with_undo);
1004+
execute_script(&selected_script, with_undo);
9521005
}
9531006

9541007
void execute_script_at(ssize_t n)

test_scripts/trigger-file/dep.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
try:
2+
trigger_ver += 1
3+
except:
4+
trigger_ver = 1
5+
6+
print(f"dep file version {trigger_ver}: Even if you save this file, this script won't re-execute. Instead, create the trigger file.")

test_scripts/trigger-file/trigger.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import idaapi
2+
import dep
3+
4+
try:
5+
trigger_ver += 1
6+
except:
7+
trigger_ver = 1
8+
9+
print(f"version {trigger_ver}: Even if you save this file, this script won't re-execute. Instead, create the trigger file.")
10+
11+
print("----------------------")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/reload import imp;imp.reload($basename$);
2+
/triggerfile createme.tmp
3+
dep.py

utils_impl.cpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,22 @@ struct collect_extlangs: extlang_visitor_t
2121
// Utility function to return a file's last modification timestamp
2222
bool get_file_modification_time(
2323
const char *filename,
24-
qtime64_t &mtime)
24+
qtime64_t *mtime = nullptr)
2525
{
2626
qstatbuf stat_buf;
2727
if (qstat(filename, &stat_buf) != 0)
2828
return false;
29-
else
30-
return mtime = stat_buf.qst_mtime, true;
29+
30+
if (mtime != nullptr)
31+
*mtime = stat_buf.qst_mtime;
32+
return true;
33+
}
34+
35+
bool get_file_modification_time(
36+
const qstring &filename,
37+
qtime64_t *mtime = nullptr)
38+
{
39+
return get_file_modification_time(filename.c_str(), mtime);
3140
}
3241

3342
//-------------------------------------------------------------------------

0 commit comments

Comments
 (0)