14.8. Breakpoints
Prev | Chapter 14. Lua Debugger | Next

## 14.8. Breakpoints

The Breakpoints section lists every breakpoint the debugger knows about, with two columns:

- Active — a checkbox. Unchecking disables the breakpoint without removing it.
- Location — `source:line` for the line of the breakpoint.

Rows whose source file no longer exists on disk are drawn with a warning icon, gray text, and a disabled Active checkbox; their tooltip reads File not found: <path> so you can spot stale entries that the loader will never resolve.

Breakpoints are created and removed in several ways:

- Click the gutter of the editor next to a line to toggle a breakpoint. A red circle appears in the gutter for active breakpoints; an inactive breakpoint is drawn as a gray circle. `Shift`-clicking the gutter on an existing breakpoint toggles only its active state (red ↔ gray) without removing it; on a line with no breakpoint, `Shift`-click adds a disabled (gray) breakpoint, which is handy for pre-arming a line you want to break on later without paying the line-hook cost until you activate it. The gutter’s tooltip surfaces both behaviors.
- Use the editor context menu: Add Breakpoint / Remove Breakpoint on the line under the cursor, or with the script editor focused use `Ctrl+Shift+B` to toggle a breakpoint on the current line (same as the context-menu shortcuts).
- Run to this line (toolbar, editor context menu; only while paused) sets a one-shot target on that line and resumes; with the script editor focused, `Ctrl+F10` does the same. The target clears itself the first time execution reaches it, and is also discarded by Continue or any Step. The target survives across dissector returns, so a line that is only reached on a later packet still pauses.

Looking for conditional breakpoints, hit counts, or logpoints? Edit the Location cell of a breakpoint row to attach any of those — see Section 14.8.1, “Conditions, hit counts, and logpoints”.

Double-clicking a row in the Breakpoints table opens the corresponding file in the editor and jumps to the line.

The section header carries four small controls to the right of the title rule, in left-to-right order:

- a colored dot — the aggregate-active toggle. The dot mirrors the gutter convention: gray when every breakpoint is inactive, red when any breakpoint is active. Clicking it flips the aggregate state in one shot — gray → activates every breakpoint, red → deactivates every breakpoint. The control is disabled (and the dot stays gray) when there are no breakpoints. Its tooltip describes the click outcome and reminds you of the per-line `Ctrl+Shift+B` shortcut.
- `－` — Remove Breakpoint (`Delete`). Removes the selected rows; disabled when nothing is selected.
- `⚙` — Edit Breakpoint. Opens the inline editor on the focused row (or the first selected row), the same as double-clicking the Location cell or pressing `F2`.
- `Ⓧ` — Remove All Breakpoints (`Ctrl+Shift+F9`). Disabled when the Breakpoints list is empty.

The Breakpoints table supports `Ctrl` / `Shift` click for multi-row selection. A right-click context menu groups its entries by scope — first the actions that target the row under the cursor, then the actions that target the whole table:

- Edit…​ — opens the inline editor on the clicked row, same as double-clicking Location or pressing `F2`. Disabled on stale rows whose target file is no longer loaded.
- Open Source — jumps the editor to the breakpoint’s file and line. Double-clicking Location also navigates here, in addition to opening the inline editor.
- Reset Hit Count — clears `hit_count` and the `{delta}` reference timestamp for the clicked row. Enabled when the row has a hit-count target or a counter greater than zero.
- Remove — drops the selected rows (`Delete` / `Backspace` also work).
- Reset All Hit Counts — clears `hit_count` and the `{delta}` reference timestamp on every row that currently has a non-zero counter.
- Remove All Breakpoints — drops the whole list, same as the `Ⓧ` control in the section header (labeled with `Ctrl+Shift+F9` in the menu; the shortcut works from anywhere in the dialog).

Breakpoints survive Wireshark restarts. Adding a breakpoint automatically turns the debugger on if it was off — running with no line hook cannot trigger a pause.

### 14.8.1. Conditions, hit counts, and logpoints

The Location cell of a breakpoint row is editable (double-click, `F2`, or right-click → Edit) and lets you attach one of three extras to the breakpoint. A white core inside the breakpoint dot — in the Breakpoints list and in the editor gutter — marks rows that carry an extra:

- Expression — a Lua expression evaluated each time control reaches the line. The breakpoint pauses only when the expression is truthy in the current frame; locals, upvalues, and globals are visible exactly as they are in Watch / Evaluate. Runtime errors are treated as false (no pause) and surface as a warning icon on the row.

- Hit Count — gate the pause on a hit-counter. `0` disables the gate. A small dropdown next to the integer picks the comparison mode:

  `from` | Pause on every hit from N onwards — inclusive of the N-th hit (default; "skip the first N-1, then pause forever").
  `every` | Pause on hits N, 2N, 3N, … — sample a noisy loop without stopping on every iteration.
  `once` | One-shot. Pause on the N-th hit and deactivate the breakpoint so subsequent hits go through. The slot is consumed the moment the counter reaches N, even if the row’s Expression evaluates falsy or the row is a logpoint that emits and resumes — the row deactivates either way. The row’s Active checkbox visibly clears AND the runtime counter resets to zero in the same step, so re-ticking Active is enough to arm the next N-hit cycle without a separate Reset Hit Count.

  The counter is per breakpoint and is not persisted across Wireshark restarts.

- Log Message — a template that is written to the Evaluate output (and to Wireshark’s debug log) each time the breakpoint fires — that is, after the Hit Count gate and any Expression allow it. By default execution continues without pausing; click the pause toggle on the editor row (the icon next to the Log Message field) to also pause after emitting (useful for "log-then-inspect" without duplicating the breakpoint). The line is emitted verbatim — there is no automatic file:line prefix; include the origin via the tags below if you want it.

The Log Message template uses `{…​}` placeholders. `{{` and `}}` produce literal `{` / `}`. Anything inside `{}` that is not one of the reserved tags below is evaluated as a Lua expression in the paused frame and converted to text with the same coercion `tostring()` performs; per-placeholder evaluation errors substitute `<error: …​>` without aborting the line.

The reserved tags below shadow any same-named Lua local / upvalue / global. To log a Lua variable that happens to share a name with a tag, wrap it: `{tostring(filename)}`.

Origin

`{filename}` | Source file path (canonicalized).
`{basename}` | Last path component of `{filename}`, e.g. `printer.lua`. Empty when the chunk has no on-disk path.
`{line}` | 1-based line number of the breakpoint.
`{function}` | Running function’s name (`debug.getinfo` `n`), or `?` for anonymous, tail, and main-chunk frames.
`{what}` | Frame kind: `Lua`, `C`, `main`, or `tail`.

Counters and scope

`{hits}` | This breakpoint’s cumulative hit counter — the number of times the line has been reached since the row was created or the counter was last reset. The counter advances on every line hit, including hits skipped by the hit-count gate or a falsy Expression; the log line itself only emits on hits the gates pass, so the value rendered is the counter at the firing moment (e.g. `every 100` shows `100, 200, 300, …`).
`{depth}` | Lua-frame stack depth at the fire site.
`{thread}` | `main` for the main thread, `coro@<ptr>` for coroutines (the pointer is stable per coroutine within a session).

Time

`{timestamp}` | Local wall-clock `HH:MM:SS.mmm` at fire time.
`{datetime}` | Local `YYYY-MM-DD HH:MM:SS.mmm` at fire time.
`{epoch}` | Unix time, seconds with millisecond fraction.
`{epoch_ms}` | Unix time as integer milliseconds.
`{elapsed}` | Milliseconds since the debugger was last attached.
`{delta}` | Milliseconds since this breakpoint last fired (`0` on the first fire and after Reset Hit Count / Reset All Hit Counts).

#### 14.8.1.1. Examples

The mini-blocks below resolve against the names defined by the `printer.lua` postdissector in Section 14.3, “Getting Started”: locals `tvb`, `pinfo`, `tree`, `offset` inside `printer.dissector`, upvalues `sessions`, `last_seen`, `opcode_names`, `handshake`, `ip_src_F`, `tcp_flags_F`. Drop a breakpoint on any line of `printer.dissector` and edit its Location cell to attach the extras shown.

Expression — gate the pause on a Lua predicate.

    -- Pause only after frame 100:
    pinfo.number > 100

    -- Only on the first dissection of a frame:
    not pinfo.visited

    -- Only TLS in either direction:
    pinfo.dst_port == 443 or pinfo.src_port == 443

    -- Frames whose IP source we have seen before:
    last_seen[tostring(pinfo.src)] ~= nil

    -- Bail-out invariant: pause if a length assumption breaks:
    tvb:len() < 20

A runtime error inside the expression is treated as `false` and shows a warning icon on the row, so a typo or a `nil` index silently disables the pause instead of stopping execution. See Section 14.6.3, “Expression patterns” for the full menu of shapes the same evaluator accepts.

Hit Count — gate the pause on a counter.

The integer field carries N; the dropdown next to it picks the comparison mode. The block below sketches the three shapes the gate can take (each line reads as "mode N" — the mode is the dropdown selection, N is what you type into the integer field):

    from 10   -- ignore the first 9 hits, pause from hit 10 on
                 (default mode; "skip the warm-up")
    every 100 -- sample a tight per-packet loop: pause on hits
                 100, 200, 300, …
    once 5    -- one-shot debug: pause on hit 5 and clear the
                 row's Active checkbox; tick it again to re-arm
    0         -- disable the gate (any mode); same as clearing
                 the field

The counter is per breakpoint and per Wireshark session. It is preserved across edits to the Expression, the Hit Count target or mode, and the Log Message — tuning the threshold or switching from `from` to `every` mid-run will not throw away the count you were watching, with one exception: lowering the target below the current counter rolls the counter back to `0` so the breakpoint can resume waiting for "the next N hits" instead of pausing on every line. Right-click the row and choose Reset Hit Count — or Reset All Hit Counts on the empty area of the table — to zero it explicitly. The counter is not persisted across Wireshark restarts. Hit Count, Expression, and Log Message on the same row run in a strict order: hit-count → Expression → Log Message. The hit-count gate runs first; only when it passes is the Expression evaluated; only when the Expression is truthy (or absent) does the Log Message emit and the row pause. So a logpoint with a condition stays silent while the condition is false, and an `every 100` logpoint with `tvb:len() > 1500` only logs on the matching every-hundredth packet.

Log Message — show where the line hit.

    {filename}:{line}
    {basename}:{line} in {function}
    {filename}:{line} ({what}) hits={hits} depth={depth}
    [{thread}] {function}:{line}

`{basename}` is the readable form of `{filename}` — the script’s filename without its directory, useful when most logs come from one or two scripts and the absolute path is just noise. `{function}` is `?` for anonymous, tail, and main-chunk frames; `{what}` (`Lua` / `C` / `main` / `tail`) disambiguates them. `{thread}` is `main` for the main coroutine and `coro@<ptr>` otherwise — handy when a dissector is called from a tap callback that runs on its own coroutine.

Log Message — wall-clock and intervals.

    [{timestamp}] hit
    [{datetime}] entered {function}
    {epoch_ms} -- frame seen, ms since the Unix epoch
    {elapsed} ms since the debugger was attached
    loop iteration {hits} took {delta} ms since the previous fire

Use `{timestamp}` for short interactive sessions and `{datetime}` for long-running captures where you may cross midnight. `{delta}` reports `0` on the very first fire (and after Reset Hit Count / Reset All Hit Counts), so a "took 0 ms" line in the log marks a fresh counter rather than an instant fire.

Log Message — mixing tags with Lua expressions.

Anything inside `{}` that isn’t a reserved tag is evaluated as a Lua expression in the paused frame and converted to text with the same coercion `tostring()` performs, just like a Watch row.

    frame {pinfo.number}: {tostring(pinfo.src)} -> {tostring(pinfo.dst)}
    bytes_left={tvb:len() - offset} at {filename}:{line}
    flags=0x{string.format("%02x", tvb:range(13, 1):uint())}
    {tostring(pinfo.src)} last seen at frame {last_seen[tostring(pinfo.src)] or "never"}
    {datetime} hit #{hits}: ports {pinfo.src_port}->{pinfo.dst_port}

Wireshark userdata classes (`Address`, `Pinfo`, `Tvb`, …) expose their canonical text via `__tostring`, so wrap them in `tostring(…​)` instead of relying on a `:tostring()` method (the class doesn’t have one). `{` is the only character that needs escaping; `{{` and `}}` produce literal braces:

    state = {{ src={tostring(pinfo.src)}, dst={tostring(pinfo.dst)} }}

A per-placeholder runtime error becomes `<error: …>` inline, so one bad expression in a long template doesn’t drop the rest of the line. Edit the row’s Location cell to clear the bad placeholder.

| Logpoints on every packet of a large capture
A logpoint that matches every packet of a multi-thousand-frame capture can fire thousands of times per second. The debugger coalesces those fires onto the GUI thread in a single drain per event-loop tick, and the Evaluate output keeps only the last ~5000 lines (older lines are evicted as new ones arrive), so the dialog stays responsive — but each fire still runs the template formatter on the Lua thread. To keep per-packet logpoints cheap on big captures: Throttle with Hit Count: pick `every` N (e.g. `every 100`) to sample one out of every N matches instead of all of them. Filter with Expression: a predicate like `pinfo.dst_port == 443` or `tvb:len() > 1500` skips the format-and-emit path entirely on every non-matching frame. Keep templates lean: `{depth}` walks the Lua call stack and `{thread}` queries the running coroutine on every fire. Tags the template doesn’t reference are skipped, so omit ones you don’t need.

Prev | Up | Next
14.7. Stack Trace | Home | 14.9. Files