Coverage Report

Created: 2026-05-12 06:44

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/zeek/src/Trigger.cc
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