Line | Count | Source |
1 | | // See the file "COPYING" in the main distribution directory for copyright. |
2 | | |
3 | | #include "zeek/Trigger.h" |
4 | | |
5 | | #include <algorithm> |
6 | | #include <cassert> |
7 | | |
8 | | #include "zeek/DebugLogger.h" |
9 | | #include "zeek/Desc.h" |
10 | | #include "zeek/Expr.h" |
11 | | #include "zeek/Frame.h" |
12 | | #include "zeek/ID.h" |
13 | | #include "zeek/Reporter.h" |
14 | | #include "zeek/Stmt.h" |
15 | | #include "zeek/Traverse.h" |
16 | | #include "zeek/Val.h" |
17 | | #include "zeek/iosource/Manager.h" |
18 | | #include "zeek/telemetry/Manager.h" |
19 | | |
20 | | using namespace zeek::detail; |
21 | | using namespace zeek::detail::trigger; |
22 | | |
23 | | // Callback class to traverse an expression, registering all relevant IDs and |
24 | | // Vals for change notifications. |
25 | | |
26 | | namespace zeek::detail::trigger { |
27 | | |
28 | | // Used to extract the globals and locals seen in a trigger expression. |
29 | | class TriggerTraversalCallback : public TraversalCallback { |
30 | | public: |
31 | 0 | TriggerTraversalCallback(IDSet& _globals, IDSet& _locals) : globals(_globals), locals(_locals) {} |
32 | | |
33 | | TraversalCode PreExpr(const Expr*) override; |
34 | | |
35 | | private: |
36 | | IDSet& globals; |
37 | | IDSet& locals; |
38 | | }; |
39 | | |
40 | 0 | TraversalCode trigger::TriggerTraversalCallback::PreExpr(const Expr* expr) { |
41 | | // We catch all expressions here which in some way reference global |
42 | | // state. |
43 | |
|
44 | 0 | switch ( expr->Tag() ) { |
45 | 0 | case EXPR_NAME: { |
46 | 0 | const auto* e = static_cast<const NameExpr*>(expr); |
47 | 0 | auto id = e->IdPtr(); |
48 | |
|
49 | 0 | if ( id->IsGlobal() ) |
50 | 0 | globals.insert(id); |
51 | 0 | else |
52 | 0 | locals.insert(id); |
53 | 0 | }; |
54 | |
|
55 | 0 | default: |
56 | | // All others are uninteresting. |
57 | 0 | break; |
58 | 0 | } |
59 | | |
60 | 0 | return TC_CONTINUE; |
61 | 0 | } |
62 | | |
63 | | class TriggerTimer final : public Timer { |
64 | | public: |
65 | | TriggerTimer(double arg_timeout, Trigger* arg_trigger) |
66 | 0 | : Timer(run_state::network_time + arg_timeout, TIMER_TRIGGER) { |
67 | 0 | Ref(arg_trigger); |
68 | 0 | trigger = arg_trigger; |
69 | 0 | timeout = arg_timeout; |
70 | 0 | time = run_state::network_time; |
71 | 0 | } |
72 | | |
73 | 0 | ~TriggerTimer() override { Unref(trigger); } |
74 | | |
75 | 0 | void Dispatch(double t, bool is_expire) override { |
76 | | // The network_time may still have been zero when the |
77 | | // timer was instantiated. In this case, it fires |
78 | | // immediately and we simply restart it. |
79 | 0 | if ( time ) |
80 | 0 | trigger->Timeout(); |
81 | 0 | else { |
82 | 0 | TriggerTimer* timer = new TriggerTimer(timeout, trigger); |
83 | 0 | timer_mgr->Add(timer); |
84 | 0 | trigger->timer = timer; |
85 | 0 | } |
86 | 0 | } |
87 | | |
88 | | protected: |
89 | | Trigger* trigger; |
90 | | double timeout; |
91 | | double time; |
92 | | }; |
93 | | |
94 | | Trigger::Trigger(const std::shared_ptr<WhenInfo>& wi, const IDSet& _globals, std::vector<ValPtr> _local_aggrs, |
95 | 134k | double timeout, Frame* f, const Location* loc) { |
96 | 134k | timeout_value = timeout; |
97 | 134k | globals = _globals; |
98 | 134k | local_aggrs = std::move(_local_aggrs); |
99 | 134k | have_trigger_elems = true; |
100 | | |
101 | 134k | cond = wi->Cond(); |
102 | 134k | body = wi->WhenBody(); |
103 | 134k | timeout_stmts = wi->TimeoutStmt(); |
104 | 134k | is_return = wi->IsReturn(); |
105 | | |
106 | 134k | timer = nullptr; |
107 | 134k | delayed = false; |
108 | 134k | disabled = false; |
109 | 134k | attached = nullptr; |
110 | | |
111 | 134k | if ( loc ) |
112 | 134k | name = util::fmt("%s:%d-%d", loc->FileName(), loc->FirstLine(), loc->LastLine()); |
113 | 0 | else |
114 | 0 | name = "<no-trigger-location>"; |
115 | | |
116 | 134k | if ( f ) |
117 | 134k | frame = f->CloneForTrigger(); |
118 | 0 | else |
119 | 0 | frame = nullptr; |
120 | | |
121 | 134k | DBG_LOG(DBG_NOTIFIERS, "%s: instantiating", Name()); |
122 | | |
123 | 134k | if ( is_return && frame ) { |
124 | 0 | Trigger* parent = frame->GetTrigger(); |
125 | 0 | if ( ! parent ) { |
126 | 0 | reporter->Error("return trigger in context which does not allow delaying result"); |
127 | 0 | return; |
128 | 0 | } |
129 | | |
130 | 0 | parent->Attach(this); |
131 | 0 | f->SetDelayed(); |
132 | 0 | } |
133 | | |
134 | | // Make sure we don't get deleted if somebody calls a method like |
135 | | // Timeout() while evaluating the trigger. |
136 | 134k | Ref(this); |
137 | | |
138 | 134k | if ( ! Eval() && timeout_value >= 0 ) { |
139 | 0 | timer = new TriggerTimer(timeout_value, this); |
140 | 0 | timer_mgr->Add(timer); |
141 | 0 | } |
142 | 134k | } |
143 | | |
144 | 0 | void Trigger::Terminate() { |
145 | 0 | if ( is_return ) { |
146 | 0 | auto parent = frame->GetTrigger(); |
147 | |
|
148 | 0 | if ( ! parent->Disabled() ) { |
149 | | // If the trigger was already disabled due to interpreter |
150 | | // exception, an Unref already happened at that point. |
151 | 0 | parent->Disable(); |
152 | 0 | Unref(parent); |
153 | 0 | } |
154 | |
|
155 | 0 | frame->ClearTrigger(); |
156 | 0 | } |
157 | |
|
158 | 0 | Disable(); |
159 | 0 | Unref(this); |
160 | 0 | } |
161 | | |
162 | 134k | Trigger::~Trigger() { |
163 | 134k | DBG_LOG(DBG_NOTIFIERS, "%s: deleting", Name()); |
164 | | |
165 | 134k | for ( auto& [_, trigger] : cache ) |
166 | 134k | Unref(trigger); |
167 | | |
168 | 134k | Unref(frame); |
169 | 134k | UnregisterAll(); |
170 | | |
171 | 134k | Unref(attached); |
172 | | // Due to ref'counting, "this" cannot be part of pending at this |
173 | | // point. |
174 | 134k | } |
175 | | |
176 | 0 | void Trigger::ReInit(const std::vector<ValPtr>& index_expr_results) { |
177 | 0 | assert(! disabled); |
178 | 0 | UnregisterAll(); |
179 | |
|
180 | 0 | if ( ! have_trigger_elems ) { |
181 | 0 | TriggerTraversalCallback cb(globals, locals); |
182 | 0 | cond->Traverse(&cb); |
183 | 0 | have_trigger_elems = true; |
184 | 0 | } |
185 | |
|
186 | 0 | for ( const auto& g : globals ) { |
187 | 0 | Register(g.get()); |
188 | |
|
189 | 0 | auto& v = g->GetVal(); |
190 | 0 | if ( v && v->Modifiable() ) |
191 | 0 | Register(v.get()); |
192 | 0 | } |
193 | |
|
194 | 0 | for ( const auto& l : locals ) { |
195 | 0 | ASSERT(! l->GetVal()); |
196 | 0 | } |
197 | | |
198 | 0 | for ( auto& av : local_aggrs ) |
199 | 0 | Register(av.get()); |
200 | |
|
201 | 0 | for ( const auto& v : index_expr_results ) |
202 | 0 | Register(v.get()); |
203 | 0 | } |
204 | | |
205 | 269k | bool Trigger::Eval() { |
206 | 269k | if ( disabled ) |
207 | 0 | return true; |
208 | | |
209 | 269k | DBG_LOG(DBG_NOTIFIERS, "%s: evaluating", Name()); |
210 | | |
211 | 269k | if ( delayed ) { |
212 | 0 | DBG_LOG(DBG_NOTIFIERS, "%s: skipping eval due to delayed call", Name()); |
213 | 0 | return false; |
214 | 0 | } |
215 | | |
216 | | // It's unfortunate that we have to copy the frame again here but |
217 | | // otherwise changes to any of the locals would propagate to later |
218 | | // evaluations. |
219 | | // |
220 | | // An alternative approach to copying the frame would be to deep-copy |
221 | | // the expression itself, replacing all references to locals with |
222 | | // constants. |
223 | | |
224 | 269k | Frame* f = nullptr; |
225 | | |
226 | 269k | try { |
227 | 269k | f = frame->CloneForTrigger(); |
228 | 269k | } catch ( InterpreterException& ) { |
229 | | // Frame contains values that couldn't be cloned. It's |
230 | | // already been reported, disable trigger. |
231 | 0 | Disable(); |
232 | 0 | Unref(this); |
233 | 0 | return false; |
234 | 0 | } |
235 | | |
236 | 269k | f->SetTrigger({NewRef{}, this}); |
237 | | |
238 | 269k | ValPtr v; |
239 | 269k | IndexExprWhen::StartEval(); |
240 | | |
241 | 269k | try { |
242 | 269k | v = cond->Eval(f); |
243 | 269k | } catch ( InterpreterException& ) { /* Already reported */ |
244 | 0 | } |
245 | | |
246 | 269k | IndexExprWhen::EndEval(); |
247 | 269k | auto index_expr_results = IndexExprWhen::TakeAllResults(); |
248 | | |
249 | 269k | f->ClearTrigger(); |
250 | | |
251 | 269k | if ( f->HasDelayed() ) { |
252 | 134k | DBG_LOG(DBG_NOTIFIERS, "%s: eval has delayed", Name()); |
253 | 134k | assert(! v); |
254 | 134k | Unref(f); |
255 | 134k | return false; |
256 | 134k | } |
257 | | |
258 | 134k | if ( ! v || v->IsZero() ) { |
259 | | // Not true. Perhaps next time... |
260 | 0 | DBG_LOG(DBG_NOTIFIERS, "%s: trigger condition is false", Name()); |
261 | 0 | Unref(f); |
262 | 0 | ReInit(index_expr_results); |
263 | 0 | return false; |
264 | 0 | } |
265 | | |
266 | 134k | DBG_LOG(DBG_NOTIFIERS, "%s: trigger condition is true, executing", Name()); |
267 | | |
268 | 134k | v = nullptr; |
269 | 134k | StmtFlowType flow; |
270 | | |
271 | 134k | try { |
272 | 134k | v = body->Exec(f, flow); |
273 | 134k | } catch ( InterpreterException& e ) { /* Already reported. */ |
274 | 0 | } |
275 | | |
276 | 134k | if ( is_return ) { |
277 | 0 | Trigger* trigger = frame->GetTrigger(); |
278 | 0 | assert(trigger); |
279 | 0 | assert(frame->GetTriggerAssoc()); |
280 | 0 | assert(trigger->attached == this); |
281 | | |
282 | 0 | #ifdef DEBUG |
283 | 0 | const char* pname = util::copy_string(trigger->Name()); |
284 | 0 | DBG_LOG(DBG_NOTIFIERS, "%s: trigger has parent %s, caching result", Name(), pname); |
285 | 0 | delete[] pname; |
286 | 0 | #endif |
287 | |
|
288 | 0 | auto queued = trigger->Cache(frame->GetTriggerAssoc(), v.get()); |
289 | 0 | trigger->Release(); |
290 | 0 | frame->ClearTrigger(); |
291 | |
|
292 | 0 | if ( ! queued && trigger->TimeoutValue() < 0 ) |
293 | | // Usually the parent-trigger would get unref'd either by |
294 | | // its Eval() or its eventual Timeout(), but has neither |
295 | 0 | Unref(trigger); |
296 | 0 | } |
297 | | |
298 | 134k | Unref(f); |
299 | | |
300 | 134k | if ( timer ) |
301 | 0 | timer_mgr->Cancel(timer); |
302 | | |
303 | 134k | Disable(); |
304 | 134k | Unref(this); |
305 | | |
306 | 134k | return true; |
307 | 134k | } |
308 | | |
309 | 0 | void Trigger::Timeout() { |
310 | 0 | if ( disabled ) |
311 | 0 | return; |
312 | | |
313 | 0 | DBG_LOG(DBG_NOTIFIERS, "%s: timeout", Name()); |
314 | 0 | if ( timeout_stmts ) { |
315 | 0 | StmtFlowType flow; |
316 | 0 | FramePtr f{AdoptRef{}, frame->CloneForTrigger()}; |
317 | 0 | ValPtr v; |
318 | |
|
319 | 0 | try { |
320 | 0 | v = timeout_stmts->Exec(f.get(), flow); |
321 | 0 | } catch ( InterpreterException& e ) { /* Already reported. */ |
322 | 0 | } |
323 | |
|
324 | 0 | if ( is_return ) { |
325 | 0 | Trigger* trigger = frame->GetTrigger(); |
326 | 0 | assert(trigger); |
327 | 0 | assert(frame->GetTriggerAssoc()); |
328 | 0 | assert(trigger->attached == this); |
329 | | |
330 | 0 | #ifdef DEBUG |
331 | 0 | const char* pname = util::copy_string(trigger->Name()); |
332 | 0 | DBG_LOG(DBG_NOTIFIERS, "%s: trigger has parent %s, caching timeout result", Name(), pname); |
333 | 0 | delete[] pname; |
334 | 0 | #endif |
335 | 0 | auto queued = trigger->Cache(frame->GetTriggerAssoc(), v.get()); |
336 | 0 | trigger->Release(); |
337 | 0 | frame->ClearTrigger(); |
338 | |
|
339 | 0 | if ( ! queued && trigger->TimeoutValue() < 0 ) |
340 | | // Usually the parent-trigger would get unref'd either by |
341 | | // its Eval() or its eventual Timeout(), but has neither |
342 | 0 | Unref(trigger); |
343 | 0 | } |
344 | 0 | } |
345 | | |
346 | 0 | Disable(); |
347 | 0 | Unref(this); |
348 | 0 | } |
349 | | |
350 | 0 | void Trigger::Register(const ID* const_id) { |
351 | 0 | assert(! disabled); |
352 | 0 | ID* id = const_cast<ID*>(const_id); |
353 | 0 | notifier::detail::registry.Register(id, this); |
354 | |
|
355 | 0 | Ref(id); |
356 | 0 | objs.emplace_back(id, id); |
357 | 0 | } |
358 | | |
359 | 0 | void Trigger::Register(Val* val) { |
360 | 0 | if ( ! val->Modifiable() ) |
361 | 0 | return; |
362 | | |
363 | 0 | assert(! disabled); |
364 | 0 | notifier::detail::registry.Register(val->Modifiable(), this); |
365 | |
|
366 | 0 | Ref(val); |
367 | 0 | objs.emplace_back(val, val->Modifiable()); |
368 | 0 | } |
369 | | |
370 | 269k | void Trigger::UnregisterAll() { |
371 | 269k | DBG_LOG(DBG_NOTIFIERS, "%s: unregistering all", Name()); |
372 | | |
373 | 269k | for ( const auto& o : objs ) { |
374 | 0 | notifier::detail::registry.Unregister(o.second, this); |
375 | 0 | Unref(o.first); |
376 | 0 | } |
377 | | |
378 | 269k | objs.clear(); |
379 | 269k | } |
380 | | |
381 | 0 | void Trigger::Attach(Trigger* trigger) { |
382 | 0 | assert(! disabled); |
383 | 0 | assert(! trigger->disabled); |
384 | 0 | assert(! trigger->delayed); |
385 | | |
386 | 0 | #ifdef DEBUG |
387 | 0 | const char* pname = util::copy_string(trigger->Name()); |
388 | 0 | DBG_LOG(DBG_NOTIFIERS, "%s: attaching to %s", Name(), pname); |
389 | 0 | delete[] pname; |
390 | 0 | #endif |
391 | |
|
392 | 0 | Ref(trigger); |
393 | 0 | attached = trigger; |
394 | 0 | Hold(); |
395 | 0 | } |
396 | | |
397 | 134k | bool Trigger::Cache(const void* obj, Val* v) { |
398 | 134k | if ( disabled || ! v ) |
399 | 0 | return false; |
400 | | |
401 | 134k | ValCache::iterator i = cache.find(obj); |
402 | | |
403 | 134k | if ( i != cache.end() ) { |
404 | 0 | Unref(i->second); |
405 | 0 | i->second = v; |
406 | 0 | } |
407 | | |
408 | 134k | else |
409 | 134k | cache.insert(ValCache::value_type(obj, v)); |
410 | | |
411 | 134k | Ref(v); |
412 | | |
413 | 134k | trigger_mgr->Queue(this); |
414 | 134k | return true; |
415 | 134k | } |
416 | | |
417 | 539k | Val* Trigger::Lookup(const void* obj) { |
418 | 539k | assert(! disabled); |
419 | | |
420 | 539k | ValCache::iterator i = cache.find(obj); |
421 | 539k | return (i != cache.end()) ? i->second : 0; |
422 | 539k | } |
423 | | |
424 | 134k | void Trigger::Disable() { |
425 | 134k | UnregisterAll(); |
426 | 134k | disabled = true; |
427 | 134k | } |
428 | | |
429 | 0 | void Trigger::Describe(ODesc* d) const { d->Add("<trigger>"); } |
430 | | |
431 | 0 | void Trigger::Modified(notifier::detail::Modifiable* m) { trigger_mgr->Queue(this); } |
432 | | |
433 | 66 | Manager::Manager() : iosource::IOSource() { pending = new TriggerList(); } |
434 | | |
435 | 0 | Manager::~Manager() { delete pending; } |
436 | | |
437 | 66 | void Manager::InitPostScript() { |
438 | 66 | trigger_count = telemetry_mgr->CounterInstance("zeek", "triggers", {}, "Total number of triggers scheduled"); |
439 | 66 | trigger_pending = |
440 | 66 | telemetry_mgr->GaugeInstance("zeek", "pending_triggers", {}, "Pending number of triggers", "", []() { |
441 | 0 | return trigger_mgr ? static_cast<double>(trigger_mgr->pending->size()) : 0.0; |
442 | 0 | }); |
443 | | |
444 | 66 | iosource_mgr->Register(this, true); |
445 | 66 | } |
446 | | |
447 | 0 | double Manager::GetNextTimeout() { return pending->empty() ? -1 : run_state::network_time + 0.100; } |
448 | | |
449 | 4.96M | void Manager::Process() { |
450 | 4.96M | DBG_LOG(DBG_NOTIFIERS, "evaluating all pending triggers"); |
451 | | |
452 | | // While we iterate over the list, executing statements, we may |
453 | | // in fact trigger new triggers and thereby modify the list. |
454 | | // Therefore, we create a new temporary list which will receive |
455 | | // triggers triggered during this time. |
456 | 4.96M | TriggerList* orig = pending; |
457 | 4.96M | TriggerList tmp; |
458 | 4.96M | pending = &tmp; |
459 | | |
460 | 4.96M | for ( auto* t : *orig ) { |
461 | 134k | t->Eval(); |
462 | 134k | Unref(t); |
463 | 134k | } |
464 | | |
465 | 4.96M | pending = orig; |
466 | 4.96M | orig->clear(); |
467 | | |
468 | 4.96M | std::swap(tmp, *pending); |
469 | 4.96M | } |
470 | | |
471 | 134k | void Manager::Queue(Trigger* trigger) { |
472 | 134k | if ( std::ranges::find(*pending, trigger) == pending->end() ) { |
473 | 134k | Ref(trigger); |
474 | 134k | pending->push_back(trigger); |
475 | 134k | trigger_count->Inc(); |
476 | 134k | iosource_mgr->Wakeup(Tag()); |
477 | 134k | } |
478 | 134k | } |
479 | | |
480 | 0 | void Manager::GetStats(Stats* stats) { |
481 | 0 | stats->total = static_cast<unsigned long>(trigger_count->Value()); |
482 | 0 | stats->pending = pending->size(); |
483 | 0 | } |
484 | | |
485 | | } // namespace zeek::detail::trigger |