[DTrace-devel] [PATCH v4 8/9] dtrace: Add a PCP PMDA to expose DTrace data as metrics

Alan Maguire alan.maguire at oracle.com
Wed Aug 12 13:22:09 UTC 2026


This PMDA exposes a way of running DTrace scripts through PCP via the
libdtrace Python bindings. Scripts can be registered dynamically via
`pmstore(1)` calls and optionally autostarted from an on-disk directory when
the PMDA is launched using the autostart.d/ directory.

To register and run a DTrace script to count read system calls:

$ pmstore dtrace.control.register '{"name":"read_syscalls","program":"syscall::read:entry { @c[probefunc] = count(); }", "autostart":"true"}'

Now we see the aggregation data exposed as the metric
`dtrace.scripts.data.read_syscalls.c`, with one instance per
aggregation key:

$ pminfo -f dtrace

dtrace.scripts.data.read_syscalls.c
    inst [0 or "read"] value 9536

dtrace.scripts.runtime_seconds
    inst [0 or "read_syscalls"] value 2
...

To stop, unregister

$ pmstore dtrace.control.stop read_syscalls

$ pmstore dtrace.control.unregister read_syscalls

By default, dynamic registration is restricted to root only;
other users can be added in dtrace.conf.

See pcp/README.md for details.

Signed-off-by: Alan Maguire <alan.maguire at oracle.com>
---
 pcp/Build                             |   56 ++
 pcp/Install                           |   37 +
 pcp/README.md                         |  260 ++++++
 pcp/Remove                            |   27 +
 pcp/autostart.d/.gitkeep              |    0
 pcp/dtrace.conf                       |   12 +
 pcp/examples/packet_drop_reasons.d    |   14 +
 pcp/examples/packet_drop_reasons.json |    8 +
 pcp/examples/syscall_counts.d         |   11 +
 pcp/examples/syscall_counts.json      |    8 +
 pcp/pmdadtrace.python                 | 1232 +++++++++++++++++++++++++
 11 files changed, 1665 insertions(+)
 create mode 100644 pcp/Build
 create mode 100755 pcp/Install
 create mode 100644 pcp/README.md
 create mode 100755 pcp/Remove
 create mode 100644 pcp/autostart.d/.gitkeep
 create mode 100644 pcp/dtrace.conf
 create mode 100644 pcp/examples/packet_drop_reasons.d
 create mode 100644 pcp/examples/packet_drop_reasons.json
 create mode 100644 pcp/examples/syscall_counts.d
 create mode 100644 pcp/examples/syscall_counts.json
 create mode 100755 pcp/pmdadtrace.python

diff --git a/pcp/Build b/pcp/Build
new file mode 100644
index 00000000..754db762
--- /dev/null
+++ b/pcp/Build
@@ -0,0 +1,56 @@
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+
+PMDA_NAME := dtrace
+PMDA_OBJDIR := $(DESTDIR)/var/lib/pcp/pmdas/$(PMDA_NAME)
+PMDA_AUTOSTART_OBJDIR := $(PMDA_OBJDIR)/autostart.d
+PMDA_EXAMPLE_OBJDIR := $(PMDA_OBJDIR)/examples
+
+PMDA_EXEC_SCRIPTS := Install Remove pmdadtrace.python
+PMDA_DATA_FILES := README.md dtrace.conf
+PMDA_AUTOSTART_SRC := $(wildcard pcp/autostart.d/*)
+PMDA_AUTOSTART_FILES := $(notdir $(PMDA_AUTOSTART_SRC))
+PMDA_EXAMPLE_SRC := $(wildcard pcp/examples/*)
+PMDA_EXAMPLE_FILES := $(notdir $(PMDA_EXAMPLE_SRC))
+
+PHONIES += install-pmda-$(PMDA_NAME) install-pmda
+
+install:: install-pmda
+
+install-pmda:: install-pmda-$(PMDA_NAME)
+
+install-pmda-$(PMDA_NAME)::
+	$(call describe-install-target,$(PMDA_OBJDIR),$(PMDA_EXEC_SCRIPTS) $(PMDA_DATA_FILES))
+	mkdir -p $(PMDA_OBJDIR) $(PMDA_AUTOSTART_OBJDIR)
+	for f in $(PMDA_EXEC_SCRIPTS); do \
+		install -m 755 pcp/$$f $(PMDA_OBJDIR); \
+	done
+	for f in $(PMDA_DATA_FILES); do \
+		install -m 644 pcp/$$f $(PMDA_OBJDIR); \
+	done
+ifneq ($(PMDA_AUTOSTART_FILES),)
+	$(call describe-install-target,$(PMDA_AUTOSTART_OBJDIR),$(PMDA_AUTOSTART_FILES))
+	mkdir -p $(PMDA_OBJDIR) $(PMDA_AUTOSTART_OBJDIR)
+	for f in $(PMDA_AUTOSTART_FILES); do \
+		mode=644; \
+		case $$f in \
+			*.d) mode=755 ;; \
+		*) ;; \
+		esac; \
+		install -m $$mode pcp/autostart.d/$$f $(PMDA_AUTOSTART_OBJDIR); \
+	done
+endif
+ifneq ($(PMDA_EXAMPLE_FILES),)
+	$(call describe-install-target,$(PMDA_EXAMPLE_OBJDIR),$(PMDA_EXAMPLE_FILES))
+	mkdir -p $(PMDA_OBJDIR) $(PMDA_EXAMPLE_OBJDIR)
+	for f in $(PMDA_EXAMPLE_FILES); do \
+		mode=644; \
+		case $$f in \
+			*.d) mode=755 ;; \
+		*) ;; \
+		esac; \
+		install -m $$mode pcp/examples/$$f $(PMDA_EXAMPLE_OBJDIR); \
+	done
+endif
diff --git a/pcp/Install b/pcp/Install
new file mode 100755
index 00000000..1103bbd2
--- /dev/null
+++ b/pcp/Install
@@ -0,0 +1,37 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
+#
+# Copyright (c) 2026, Oracle and/or its affiliates.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public
+# License v2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+. $PCP_DIR/etc/pcp.env
+. $PCP_SHARE_DIR/lib/pmdaproc.sh
+
+iam=dtrace
+domain=487
+pmda_interface=2
+# Extend PMNS with the shipped namespace fragment
+pmns_update=true
+pmnsfile=$iam
+python_user=root
+python_group=root
+python_opt=true
+daemon_opt=false
+
+ipc_prot="binary notready"
+
+pmdaSetup
+pmdaInstall -U root
+exit
diff --git a/pcp/README.md b/pcp/README.md
new file mode 100644
index 00000000..b8c7b012
--- /dev/null
+++ b/pcp/README.md
@@ -0,0 +1,260 @@
+# SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
+#
+# Copyright (c) 2026, Oracle and/or its affiliates.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public
+# License v2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+
+# dtrace PMDA
+
+This PMDA provides support to collect PCP metrics from DTrace scripts via the
+libdtrace Python bindings. Scripts can be registered dynamically via
+`pmstore(1)` calls and optionally autostarted from an on-disk directory when
+the PMDA is launched.
+
+# Dynamic-script authorization
+
+Dynamic script registration is enabled only for `root` by default. To change
+the policy, edit the root-owned `$PCP_PMDAS_DIR/dtrace/dtrace.conf` file:
+
+```
+[dynamic_scripts]
+enabled = true
+auth_enabled = true
+allowed_users = root,mydtraceuser
+```
+
+When authentication is enabled, every write to `dtrace.control.*` must come
+from an authenticated PCP user named in `allowed_users`. Local `pmstore`
+clients are identified using their Unix UID; remote clients require PCP
+authentication. Leave `auth_enabled` set to `true`: disabling it allows every
+client with `pmcd` store permission to run DTrace programs with the PMDA's
+privileges.
+
+# Metrics
+
+- `dtrace.control.*` metrics are writable strings consumed by the PMDA to
+  register, unregister, start, stop, or reload scripts.  Payloads for
+  `dtrace.control.register` must be JSON objects containing at least the
+  `name` and `program` fields and optional `options` (libdtrace `setopt`
+  key/value pairs), `autostart`, `pid`, `command`, and `defines` fields. `pid` attaches
+  to an existing process, while `command` is the JSON equivalent of
+  `dtrace -c`: it starts a whitespace- and quote-split command under DTrace
+  control. The two target fields are mutually exclusive and initialize
+  `$target` before the program is compiled. `defines` may be an object mapping
+  macro names to values (use `null` for an unvalued macro), or an array of
+  `NAME`/`NAME=VALUE` strings; it enables preprocessing and passes definitions
+  to cpp.
+- `dtrace.scripts.*` metrics form an instance domain over the registered
+  scripts and report each script's current state, autostart flag, last error
+  message, and runtime in seconds.
+
+For example, to create a script which counts system calls that automatically
+starts and collects metrics:
+
+```
+$ pmstore dtrace.control.register '{"name":"syscalls","program":"syscall:::entry { @c[probefunc] = count(); }", "autostart":"true"}'
+dtrace.control.register old value="" new value="{"name":"syscalls","program":"syscall:::entry { @c[probefunc] = count(); }", "autostart":"true"}"
+$ pminfo -f dtrace
+
+dtrace.scripts.data.syscalls.c
+    inst [0 or "bpf"] value 5360
+    inst [1 or "access"] value 278
+    inst [2 or "poll"] value 7949
+    inst [3 or "close"] value 7693
+    inst [4 or "mmap"] value 525
+    inst [5 or "rename"] value 26
+...
+```
+
+Each instance of the metric 'dtrace.scripts.data.syscalls.c' represents
+the aggregation keys/values associated with aggregation '@c'.
+
+Aggregation names beginning with `__` are reserved for internal DTrace use.
+They are still available to the script, but the PMDA does not publish them as
+PCP metrics.  For example, `@__scratch = count();` is intentionally hidden,
+while `@scratch = count();` is exported.
+
+To target a running process, supply its PID and use `$target` in the program:
+
+```
+$ pmstore dtrace.control.register '{"name":"target_reads","pid":1234,"program":"pid$target::read:entry { @reads = count(); }","autostart":true}'
+```
+
+To create a target process, use `command` (the JSON equivalent of `dtrace -c`):
+
+```
+$ pmstore dtrace.control.register '{"name":"sleep","command":"/bin/sleep 30","program":"pid$target::sleep:entry { @calls = count(); }","autostart":true}'
+```
+
+The register payload supports the following keys.  `name` and `program` are
+required; the other keys are optional:
+
+```json
+{
+  "name": "example",
+  "program": "syscall:::entry { @calls[PROBEFUNC] = count(); }",
+  "autostart": true,
+  "separator": ";",
+  "options": {
+    "bufsize": "4m",
+    "aggrate": "1s",
+    "quiet": true
+  },
+  "compile": {
+    "zdefs": true
+  },
+  "defines": {
+    "SAMPLE_RATE": 97,
+    "BUILD_LABEL": "production",
+    "FEATURE_ENABLED": null
+  }
+}
+```
+
+The `compile` object controls compile-time flags.  `zdefs` is equivalent to
+the `dtrace -Z` option and allows probe descriptions that match no probes.
+
+`defines` enables the C preprocessor and accepts either an object, as above,
+or an array of `NAME`/`NAME=VALUE` strings:
+
+```json
+{
+  "name": "conditional",
+  "program": "BEGIN { trace(SAMPLE_RATE); }",
+  "defines": ["SAMPLE_RATE=10", "FEATURE_ENABLED"]
+}
+```
+
+For target selection, specify exactly one of `pid` or `command`:
+
+```json
+{
+  "name": "existing-process",
+  "pid": 1234,
+  "program": "pid$target:::entry { @calls = count(); }"
+}
+```
+
+```json
+{
+  "name": "new-process",
+  "command": "/usr/bin/sleep 30",
+  "program": "pid$target:::entry { @calls = count(); }"
+}
+```
+
+To stop and unregister the script
+
+```
+$ pmstore dtrace.control.unregister syscalls
+```
+
+or to simply stop (while retaining metrics):
+
+```
+$ pmstore dtrace.control.stop syscalls
+```
+
+# Profiling
+
+It is possible to profile using stack keys, and the instance names
+that represent the call stacks can be made to be compatible with
+the expected flamegraph format of `function1;function2`.  To do this,
+the default "." key separator that is used to concatenate key values
+must be overridden with a ";" , i.e.
+
+```
+"separator":";"
+```
+
+For example:
+
+```
+$ pmstore dtrace.control.register '{"name":"profile","program":"profile:::profile-97 { @profile[stack()] = count(); }", "autostart":"true", "separator":";"}'
+dtrace.control.register old value="" new value="{"name":"profile","program":"profile:::profile-97 { @profile[stack()] = count(); }", "autostart":"true"}"
+
+$ pminfo -f dtrace
+
+dtrace.scripts.data.profile.profile
+    inst [0 or "vmlinux`entry_SYSCALL_64_after_hwframe+0x76;vmlinux`do_syscall_64+0xb1;vmlinux`x64_sys_call+0x1cc6;vmlinux`__x64_sys_read+0x1d;vmlinux`ksys_read+0x6d;vmlinux`vfs_read+0xbf;vmlinux`seq_read+0xf9;vmlinux`seq_read_iter+0x2c6;vmlinux`show_smap+0xe7;vmlinux`__show_smap+0x1d1;vmlinux`seq_put_decimal_ull_width+0xae;vmlinux`strlen+0xc"] value 1
+    inst [1 or "vmlinux`entry_SYSCALL_64_after_hwframe+0x76;vmlinux`__audit_syscall_exit+0xa"] value 1
+...
+```
+
+# Autostart
+
+Scripts placed under `$PCP_PMDAS_DIR/dtrace/autostart.d/` with a `.d`
+extension are started automatically when the PMDA becomes ready. Optional
+metadata can be provided by placing a matching `.json` file alongside the `.d`
+file; its contents should mirror the register payload structure (for example,
+to define libdtrace options).
+
+An example pair is provided:
+
+- `examples/syscall_counts.d` – counts system call entries by name.
+- `examples/syscall_counts.json` – marks the script for autostart and
+  enlarges the libdtrace buffers to reduce drops under load.
+- metrics then appear under dtrace.scripts.data.syscall_counts.counts`,
+  with one instance per aggregation key (in this case syscall name):
+
+```
+# pminfo -f dtrace.scripts.data.syscall_counts.counts
+
+dtrace.scripts.data.syscall_counts.counts
+    inst [0 or "mmap"] value 7959
+    inst [1 or "futex"] value 574830
+    inst [2 or "exit"] value 202
+    inst [3 or "dup2"] value 168
+    inst [4 or "times"] value 575
+...
+```
+
+# Installation
+
+First ensure that dtrace and its associated python bindings are installed
+and running.
+
+```
+# cd $PCP_PMDAS_DIR/dtrace
+```
+
+Check there is no clash in the Performance Metrics domain defined in
+as `domain=` in `Install`. If there is a clash, edit the file.
+
+Then run
+
+```
+   # sudo ./Install
+```
+
+Verify PMDA Is running
+
+```
+   # pminfo -f dtrace
+```
+
+# De-installation
+
+```
+# cd $PCP_PMDAS_DIR/dtrace
+# sudo ./Remove
+```
+
+# Troubleshooting
+
+ + Ensure the DTrace Python bindings are installed (`python3 -c 'import dtrace'`).
+ + Confirm the PMDA log (`$PCP_LOG_DIR/pmcd/dtrace.log`) for script errors.
+ + When debugging autostart scripts, temporarily move files out of
+   `autostart.d/` to disable them.
diff --git a/pcp/Remove b/pcp/Remove
new file mode 100755
index 00000000..c8e6315a
--- /dev/null
+++ b/pcp/Remove
@@ -0,0 +1,27 @@
+#! /bin/sh
+# SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
+#
+# Copyright (c) 2026, Oracle and/or its affiliates.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public
+# License v2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+. $PCP_DIR/etc/pcp.env
+. $PCP_SHARE_DIR/lib/pmdaproc.sh
+
+iam=dtrace
+
+pmdaSetup
+pmdaRemove
+exit
+
diff --git a/pcp/autostart.d/.gitkeep b/pcp/autostart.d/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/pcp/dtrace.conf b/pcp/dtrace.conf
new file mode 100644
index 00000000..6863cc51
--- /dev/null
+++ b/pcp/dtrace.conf
@@ -0,0 +1,12 @@
+# DTrace PMDA configuration
+#
+# Dynamic scripts execute with the PMDA's privileges.  The default policy
+# permits only the authenticated root user to control them.
+[dynamic_scripts]
+enabled = true
+
+# When enabled, only authenticated users named in allowed_users may write to
+# dtrace.control.*.  Disabling this is unsafe: any client permitted by pmcd to
+# store a metric can execute a DTrace program as the PMDA user.
+auth_enabled = true
+allowed_users = root
diff --git a/pcp/examples/packet_drop_reasons.d b/pcp/examples/packet_drop_reasons.d
new file mode 100644
index 00000000..c2516691
--- /dev/null
+++ b/pcp/examples/packet_drop_reasons.d
@@ -0,0 +1,14 @@
+/* Count packet drops by reason (SOCKET_CLOSE) etc */
+
+BEGIN
+{
+	drop_reasons = (char **)`drop_reasons_core;
+	num_core_reasons = *(size_t *)(`drop_reasons_core+sizeof(char *));
+}
+
+sdt:::kfree_skb
+/arg4 < num_core_reasons /
+{
+	reason = stringof(drop_reasons[arg4]);
+	@drops[reason] = count();
+}
diff --git a/pcp/examples/packet_drop_reasons.json b/pcp/examples/packet_drop_reasons.json
new file mode 100644
index 00000000..3b26f0f4
--- /dev/null
+++ b/pcp/examples/packet_drop_reasons.json
@@ -0,0 +1,8 @@
+{
+  "name": "packet_drop_reasons",
+  "autostart": true,
+  "options": {
+    "bufsize": "16m",
+    "aggsize": "8m"
+  }
+}
diff --git a/pcp/examples/syscall_counts.d b/pcp/examples/syscall_counts.d
new file mode 100644
index 00000000..ec475bcf
--- /dev/null
+++ b/pcp/examples/syscall_counts.d
@@ -0,0 +1,11 @@
+/* Count system call invocations per syscall name */
+
+syscall:::entry
+{
+    @counts[probefunc] = count();
+}
+
+END
+{
+    printf("Tracing stopped for syscall_counts (see PCP metrics for live data)\n");
+}
diff --git a/pcp/examples/syscall_counts.json b/pcp/examples/syscall_counts.json
new file mode 100644
index 00000000..d4248fbc
--- /dev/null
+++ b/pcp/examples/syscall_counts.json
@@ -0,0 +1,8 @@
+{
+  "name": "syscall_counts",
+  "autostart": true,
+  "options": {
+    "bufsize": "16m",
+    "aggsize": "8m"
+  }
+}
diff --git a/pcp/pmdadtrace.python b/pcp/pmdadtrace.python
new file mode 100755
index 00000000..b8429f3d
--- /dev/null
+++ b/pcp/pmdadtrace.python
@@ -0,0 +1,1232 @@
+#!/usr/bin/env pmpython
+# SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
+
+# Copyright (c) 2026, Oracle and/or its affiliates.
+
+"""DTrace Performance Metrics Domain Agent.
+
+This PMDA allows DTrace scripts to be registered dynamically through pmstore
+and supports autostarting scripts from a configuration directory. Scripts are
+executed using the libdtrace Python bindings.
+"""
+
+import atexit
+import configparser
+import json
+import os
+import pwd
+import re
+import shlex
+import threading
+import time
+from collections import defaultdict
+from numbers import Number
+from pathlib import Path
+from typing import Any, Callable, Dict, Iterable, List, NamedTuple, Optional, Set, Tuple
+
+try:
+    from dtrace import DTraceError, DTraceSession, DTraceProgram  # type: ignore
+except ImportError:  # pragma: no cover - fallback for in-tree testing
+    _HERE = Path(__file__).resolve()
+    for candidate in (_HERE.parents[2] / "bindings" / "python" / "src",):
+        if candidate.is_dir() and str(candidate) not in os.sys.path:
+            os.sys.path.insert(0, str(candidate))
+    from dtrace import DTraceError, DTraceSession, DTraceProgram  # type: ignore  # noqa: E402
+
+import cpmapi as c_api
+import cpmda
+
+_DTRACE_GLOBAL_LOCK = threading.RLock()
+from pcp.pmapi import pmContext as PCP
+from pcp.pmapi import pmUnits
+from pcp.pmda import PMDA, pmdaGetContext, pmdaIndom, pmdaInstid, pmdaMetric
+
+_SANITIZE_COMPONENT = re.compile(r"[^A-Za-z0-9_\+\.;`]+")
+_IGNORED_AGGREGATION_PREFIX = "__"
+
+def _sanitize_component(value: Any) -> str:
+    if isinstance(value, bytes):
+        value = value.decode("utf-8", errors="ignore")
+    text = str(value) if value is not None else ""
+    safe = _SANITIZE_COMPONENT.sub("_", text).strip("_")
+    return safe or "value"
+
+class AggregationEntry(NamedTuple):
+    metric: str
+    instance: str
+    script: str
+    source: str
+    value: int 
+    samples: Optional[int]
+    normal: Optional[int]
+    action: str
+    keys: Tuple[str, ...]
+
+class DataInstance:
+
+    def __init__(
+        self,
+        metric: 'DataMetric',
+        instance: str,
+        id:int,
+        value: Any
+    ) -> None:
+        self._metric = metric
+        self._instance = instance
+        self._instance_id = id
+        self._value = value
+        self._instid = pmdaInstid(self._instance_id, instance)
+        metric._data_indom_insts.append(self._instid)
+
+class DataMetric:
+
+    def __init__(
+        self,
+        metric: pmdaMetric,
+        pmid: str,
+        name: str,
+        cluster: int,
+        script: str,
+        variant: str,
+        indom: int,
+        indom_id: int,
+        metric_id: int,
+        instance_next: int,
+    ) -> None:
+        self.metric = metric
+        self.pmid = pmid
+        self.name = name
+        self.cluster = cluster
+        self.script = script
+        self.variant = variant
+        self.indom = indom
+        self.indom_id = indom_id
+        self.metric_id = metric_id
+        self._instance_next = instance_next
+        self._data_instances = {}
+        self._data_instance_ids = {}
+        self._data_indom_insts = []
+
+class ManagedDTraceScript:
+    """Wraps a DTrace script lifecycle using libdtrace."""
+
+    _START_TIMEOUT = 10.0
+    _POLL_INTERVAL = 1.0
+
+    def __init__(
+        self,
+        name: str,
+        program: str,
+        autostart: bool,
+        options: Optional[Dict[str, Any]],
+        separator: Optional[str],
+        pid: Optional[int],
+        command: Optional[List[str]],
+        defines: Optional[List[str]],
+        compile_flags: int,
+        logger,
+    ) -> None:
+        self.name = name
+        self.program = program
+        self.autostart = autostart
+        self.options = dict(options or {})
+        self.options.setdefault("quiet", True)
+        self.separator = separator or "."
+        self.pid = pid
+        self.command = list(command) if command else None
+        self.defines = list(defines or [])
+        self.compile_flags = compile_flags
+        self._log = logger
+        self._lock = threading.RLock()
+        self._thread: Optional[threading.Thread] = None
+        self._stop_thread: Optional[threading.Thread] = None
+        self._stop = threading.Event()
+        self._session: Optional[DTraceSession] = None
+        self._state = "stopped"
+        self._last_error = ""
+        self._started_at: Optional[float] = None
+        self._data_callbacks = {
+        }
+        self._aggregation_cache: Dict[str, AggregationEntry] = {}
+
+    # ------------------------------------------------------------------
+    # Public accessors
+    # ------------------------------------------------------------------
+    def metric_prefix(self) -> str:
+        return "dtrace.scripts.data." + _sanitize_component(self.name) + "."
+
+    def state(self) -> str:
+        with self._lock:
+            return self._state
+
+    def last_error(self) -> str:
+        with self._lock:
+            return self._last_error
+
+    def is_running(self) -> bool:
+        return self.state() == "running"
+
+    def runtime_seconds(self) -> int:
+        with self._lock:
+            if self._state != "running" or self._started_at is None:
+                return 0
+            return int(time.time() - self._started_at)
+
+    # ------------------------------------------------------------------
+    # Lifecycle helpers
+    # ------------------------------------------------------------------
+    def start(self) -> Tuple[bool, str]:
+        self._log(f"script {self.name} starting")
+        with self._lock:
+            if self._thread and self._thread.is_alive():
+                return True, ""
+            self._stop.clear()
+            self._state = "starting"
+            self._thread = threading.Thread(
+                target=self._run, name=f"dtrace:{self.name}", daemon=True
+            )
+            self._thread.start()
+
+        deadline = time.time() + self._START_TIMEOUT
+        while time.time() < deadline:
+            state = self.state()
+            if state == "running":
+                return True, ""
+            if state == "error":
+                return False, self.last_error() or "failed to start"
+            time.sleep(0.1)
+
+        if self.state() == "running":
+            return True, ""
+        return False, self.last_error() or "timeout waiting for DTrace startup"
+
+    def stop(self) -> None:
+        self._log(f"script {self.name} stopping")
+        with self._lock:
+            if not self._thread:
+                self._state = "stopped"
+                return
+        self._stop.set()
+
+    def update_definition(
+        self,
+        program: str,
+        options: Optional[Dict[str, Any]],
+        pid: Optional[int],
+        command: Optional[List[str]],
+        defines: Optional[List[str]],
+        compile_flags: int,
+    ) -> None:
+        restart = self.is_running()
+        if restart:
+            self.stop()
+        with self._lock:
+            self.program = program
+            if options is not None:
+                self.options = options
+            self.pid = pid
+            self.command = list(command) if command else None
+            self.defines = list(defines or [])
+            self.compile_flags = compile_flags
+        if restart:
+            ok, error = self.start()
+            if not ok:
+                self._log(f"failed to restart script {self.name}: {error}")
+
+    # ------------------------------------------------------------------
+    # Internal execution loop
+    # ------------------------------------------------------------------
+    def _run(self) -> None:
+        self._log(f"script {self.name} running")
+        target = None
+        try:
+            session = DTraceSession()
+        except Exception as exc:  # pylint: disable=broad-except
+            self._fail("session creation", str(exc))
+            return
+
+        with self._lock:
+            self._session = session
+            self._last_error = ""
+
+        try:
+            # Compilation uses global variables; lock to prevent interference
+            with _DTRACE_GLOBAL_LOCK:
+                self._apply_options(session)
+                for macro in self.defines:
+                    session.setopt("cpp", None)
+                    session.setopt("define", macro)
+                if self.pid is not None:
+                    target = session.proc_grab_pid(self.pid)
+                elif self.command is not None:
+                    target = session.proc_create(self.command)
+                compiled = session.compile(self.program, cflags=self.compile_flags)
+            session.enable(compiled)
+            session.go()
+            if target is not None:
+                session.proc_continue(target)
+            with self._lock:
+                self._state = "running"
+                self._started_at = time.time()
+
+            while not self._stop.is_set():
+                set = self._stop.wait(timeout=self._POLL_INTERVAL)
+                if set:
+                    break
+                try:
+                    session.work()
+                    session.agg_snap()
+                    updates = self._walk_aggregations(session)
+                    # encourage thread yield
+                    time.sleep(0.001)
+                except DTraceError as exc:  # pragma: no cover - runtime error path
+                    self._fail("session execution", str(exc))
+                    break
+
+        except Exception as exc:  # pylint: disable=broad-except
+            self._fail("session initiation", str(exc))
+        finally:
+            try:
+                session.stop()
+                # Ensure we get final view of data (truncated aggregations etc)
+                session.agg_snap()
+                updates = self._walk_aggregations(session)
+                self._log(f"session stopped for {self.name}")
+            except Exception:  # pragma: no cover - best effort cleanup
+                pass
+            if target is not None:
+                try:
+                    session.proc_release(target)
+                except Exception:  # pragma: no cover - best effort cleanup
+                    pass
+            try:
+                session.close()
+                self._log(f"session closed for {self.name}")
+            except Exception:  # pragma: no cover - best effort cleanup
+                pass
+            with self._lock:
+                if self._state == "running":
+                    self._state = "stopped"
+                self._session = None
+
+    def _apply_options(self, session: DTraceSession) -> None:
+        self._log(f"applying options to {self.name}")
+        if not self.options:
+            return
+        for key, value in self._normalize_options(self.options):
+            try:
+                session_setopt = session.setopt
+            except AttributeError:  # pragma: no cover - safety net
+                continue
+            try:
+                session_setopt(key, value)
+            except Exception as exc:  # pylint: disable=broad-except
+                self._log(
+                    f"unable to set option '{key}' for script {self.name}: {exc}"
+                )
+
+    @staticmethod
+    def _normalize_options(options: Dict[str, Any]) -> Iterable[Tuple[str, Optional[str]]]:
+        for key, value in options.items():
+            if isinstance(value, bool):
+                yield str(key), None if value else "0"
+            elif value is None:
+                yield str(key), None
+            else:
+                yield str(key), str(value)
+
+    def _fail(self, ctx: str, message: str) -> None:
+        with self._lock:
+            self._state = "error"
+            self._last_error = message
+        self._log(f"script {self.name} encountered an error during {ctx}: {message}")
+        with self._lock:
+            self._aggregation_cache.clear()
+
+
+    def register_data_callbacks(
+        self,
+        pmda
+    ) -> None:
+        with self._lock:
+            for key in pmda._data_callbacks.keys():
+                self._data_callbacks[key] = pmda._data_callbacks[key]
+
+    def unregister_data_callbacks(
+        self,
+        pmda
+      ):
+        remove_metrics = None
+        with self._lock:
+            if "remove_metrics" in self._data_callbacks:
+                remove_metrics = self._data_callbacks["remove_metrics"]
+            self._data_callbacks = {}
+            if remove_metrics is not None:
+                remove_metrics(self)
+
+    def latest_aggregations(self) -> Dict[str, AggregationEntry]:
+        with self._lock:
+            return dict(self._aggregation_cache)
+
+    def _walk_aggregations(self, session: DTraceSession) -> Dict[str, AggregationEntry]:
+        try:
+            snapshot = session.agg_walk()
+        except Exception as exc:  # pylint: disable=broad-except
+            self._log(f"aggregation walk failed for {self.name}: {exc}")
+            return {}
+
+        updates: Dict[Tuple, AggregationEntry] = {}
+        for record in snapshot:
+            metric_name = self._metric_name_from_record(record)
+            if not metric_name:
+                continue
+            instance_name = self._data_instance_from_record(record)
+            if not instance_name:
+                continue
+            entry = AggregationEntry(
+                metric=metric_name,
+                instance=instance_name,
+                script=self.name,
+                source=record.get("name", ""),
+                value=self._coerce_int(record.get("value")),
+                samples=self._coerce_int(record.get("samples")),
+                normal=self._coerce_int(record.get("normal")),
+                action=str(record.get("action", "")),
+                keys=tuple(str(k) for k in record.get("keys", [])),
+            )
+            updates[(metric_name, instance_name)] = entry
+        with self._lock:
+            previous = self._aggregation_cache
+            self._aggregation_cache = updates
+
+        current_keys = set(updates)
+        previous_keys = set(previous)
+        created = current_keys - previous_keys
+        removed = previous_keys - current_keys
+
+        refresh = False
+        for key in created:
+            entry = updates[key]
+            try:
+                with self._lock:
+                    if "create_metric" in self._data_callbacks:
+                        self._data_callbacks["create_metric"](entry)
+                    if "create_instance" in self._data_callbacks:
+                        self._data_callbacks["create_instance"](entry)
+                        refresh = True
+            except Exception as exc:  # pylint: disable=broad-except
+                self._log(
+                    f"create callback error for {self.name}:{entry.metric}: {exc}"
+                )
+
+        for key in current_keys & previous_keys:
+            entry = updates[key]
+            try:
+                with self._lock:
+                    if "update_instance" in self._data_callbacks:
+                        self._data_callbacks["update_instance"](entry)
+            except Exception as exc:  # pylint: disable=broad-except
+                self._log(
+                    f"update callback error for {self.name}:{entry.metric}: {exc}"
+                )
+
+        for key in removed:
+            entry = previous[key]
+            try:
+                with self._lock:
+                    if "remove_instance" in self._data_callbacks:
+                        self._data_callbacks["remove_instance"](entry)
+                        refresh = True
+            except Exception as exc:  # pylint: disable=broad-except
+                self._log(
+                    f"remove callback error for {self.name}:{entry.metric}: {exc}"
+                )
+        if refresh: 
+            with self._lock:
+                if "refresh_metrics" in self._data_callbacks:
+                    self._data_callbacks["refresh_metrics"]()
+        return updates
+
+    def _metric_name_from_record(self, record: Dict[str, Any]) -> Optional[str]:
+        source = record.get("name")
+        if not source or str(source).startswith(_IGNORED_AGGREGATION_PREFIX):
+            return None
+        return self.metric_prefix() + _sanitize_component(source)
+
+    def _data_instance_from_record(self, record: Dict[str, Any]) -> Optional[str]:
+        parts = []
+        for key in record.get("keys", []):
+            # key component may be a list (stack)
+            if isinstance(key, list):
+                k = self.separator.join(key)
+            else:
+                k = key
+            parts.append(_sanitize_component(k))
+        return self.separator.join(parts)
+
+    @staticmethod
+    def _coerce_int(value: Any) -> Optional[int]:
+        if value is None:
+            return None
+        if isinstance(value, int):
+            return value
+        if isinstance(value, Number):
+            return int(value)
+        try:
+            return int(value)
+        except (TypeError, ValueError):
+            return None
+
+class DTracePMDA(PMDA):
+    """PCP PMDA providing DTrace control and state metrics."""
+
+    class Control:
+        CLUSTER = 0
+        REGISTER = 0
+        UNREGISTER = 1
+        START = 2
+        STOP = 3
+        RELOAD = 4
+
+    class Scripts:
+        CLUSTER = 1
+        STATE = 0
+        AUTOSTART = 1
+        LAST_ERROR = 2
+        RUNTIME = 3
+
+    class Data:
+        CLUSTER = 2
+
+    def _create_metric(self, entry: AggregationEntry):
+        if entry.metric in self._data_metrics:
+            return
+        metric_id = self._metric_next
+        indom = self._indom_next
+        indom_id = self.indom(indom)
+        self.add_indom(pmdaIndom(indom_id, []), "DTrace metric data", "DTrace data for metric")
+        self._indom_next += 1
+
+        metric = pmdaMetric(
+                            self.pmid(indom, metric_id),
+                            c_api.PM_TYPE_DOUBLE,
+                            indom_id,
+                            c_api.PM_SEM_DISCRETE,
+                            self._units_none,
+        )
+        try:
+            self.add_metric(entry.metric, metric)
+        except Exception as exc:
+            self.log(f"failed to add metric {entry.metric}: {exc}")
+        self._data_metrics[entry.metric] = DataMetric(
+                                                      metric=metric,
+                                                      pmid=metric.m_desc.pmid,
+                                                      name=entry.metric,
+                                                      script=entry.script,
+                                                      cluster=self.Data.CLUSTER,
+                                                      variant=entry.source,
+                                                      indom=indom,
+                                                      indom_id=indom_id,
+                                                      metric_id=metric_id,
+                                                      instance_next=0
+        )
+        self._data_metric_ids[(indom, metric_id)] = self._data_metrics[entry.metric]
+
+    def _create_instance(self, entry: AggregationEntry):
+
+        names = (entry.metric, entry.instance)
+        if entry.metric not in self._data_metrics:
+            return
+        metric = self._data_metrics[entry.metric]
+        if entry.instance in metric._data_instances:
+            return
+        instance_id = metric._instance_next
+        metric._instance_next += 1
+        metric._data_instances[entry.instance] = DataInstance(metric,
+                                                              entry.instance,
+                                                              instance_id,
+                                                              entry.value)
+        metric._data_instance_ids[instance_id] = metric._data_instances[entry.instance]
+
+    def _update_instance(self, entry: AggregationEntry):
+        if entry.metric not in self._data_metrics:
+            return
+        metric = self._data_metrics[entry.metric]
+        if entry.instance not in metric._data_instances:
+            return
+        instance = metric._data_instances[entry.instance]
+        instance._value = entry.value
+
+    def _remove_instance_by_name(self, m:str, i:str):
+        if m not in self._data_metrics:
+            return
+        metric = self._data_metrics[m]
+        if i not in metric._data_instances:
+            return
+        instance = metric._data_instances[i]
+        if instance._instid in metric._data_indom_insts:
+            metric._data_indom_insts.remove(instance._instid)
+        if instance._instance_id in metric._data_instance_ids:
+            metric._data_instance_ids.pop(instance._instance_id, None)
+
+    def _remove_instance(self, entry: AggregationEntry):
+        self._log(f"removing instance {entry.metric}, {entry.instance}")
+        self._remove_instance_by_name(entry.metric, entry.instance)
+
+    def _remove_metrics(self, script:ManagedDTraceScript):
+        prefix = script.metric_prefix()
+        self.log(f"removing metrics for {script.name}")
+        for m in list(self._data_metrics.keys()):
+            if not m.startswith(prefix):
+                continue
+            metric = self._data_metrics[m]
+            indom = metric.indom
+            metric_id = metric.metric_id
+            self.replace_indom(metric.indom_id, [])
+            self._data_metrics.pop(m, None)
+            self._data_metric_ids.pop((indom, metric_id), None)
+            self.remove_metric(m, metric.metric)
+
+    def _refresh_metrics(self):
+        self._rebuild_data_indom()
+        self.set_notify_change()
+        cpmda.set_need_refresh()
+
+    def __init__(self, name: str, domain: int) -> None:
+        super().__init__(name, domain)
+
+        self.set_user("root")
+
+        self._lock = threading.RLock()
+        (
+            self._dynamic_scripts_enabled,
+            self._auth_enabled,
+            self._allowed_users,
+        ) = self._load_control_config()
+        self._ctx_usernames: Dict[int, str] = {}
+        self._scripts: Dict[str, ManagedDTraceScript] = {}
+        self._inst_map: Dict[Tuple, str] = {}
+        self._next_inst = 0
+        self._indom_next = 2
+        self._data_metrics: Dict[str, DataMetric] = {}
+        self._data_metric_ids: Dict[Tuple, DataMetric] = {}
+        self._metric_next = 0
+
+        self.script_indom_id = self.indom(0)
+
+        self._data_callbacks = {
+                "create_metric" : self._create_metric,
+                "remove_metrics" : self._remove_metrics,
+                "refresh_metrics" : self._refresh_metrics,
+                "create_instance" : self._create_instance,
+                "update_instance" : self._update_instance,
+                "remove_instance" : self._remove_instance
+        }
+        self.script_indom = pmdaIndom(self.script_indom_id, [])
+        self.add_indom(self.script_indom, "DTrace scripts", "Registered scripts")
+
+        self._units_none = pmUnits()
+        self._units_seconds = pmUnits(0, 1, 0, 0, c_api.PM_TIME_SEC, 0)
+
+        # Require credential PDUs to avoid TYPE-0 handshake downgrade.
+        self.set_comm_flags(cpmda.PMDA_FLAG_AUTHORIZE)
+
+        self.log(f"Adding metrics for PMDA..")
+        self.add_metric(
+            "dtrace.control.register",
+            pmdaMetric(
+                self.pmid(self.Control.CLUSTER, self.Control.REGISTER),
+                c_api.PM_TYPE_STRING,
+                c_api.PM_INDOM_NULL,
+                c_api.PM_SEM_INSTANT,
+                self._units_none,
+            ),
+            "Register a DTrace script via JSON payload",
+        )
+        self.add_metric(
+            "dtrace.control.unregister",
+            pmdaMetric(
+                self.pmid(self.Control.CLUSTER, self.Control.UNREGISTER),
+                c_api.PM_TYPE_STRING,
+                c_api.PM_INDOM_NULL,
+                c_api.PM_SEM_INSTANT,
+                self._units_none,
+            ),
+            "Unregister an existing DTrace script",
+        )
+        self.add_metric(
+            "dtrace.control.start",
+            pmdaMetric(
+                self.pmid(self.Control.CLUSTER, self.Control.START),
+                c_api.PM_TYPE_STRING,
+                c_api.PM_INDOM_NULL,
+                c_api.PM_SEM_INSTANT,
+                self._units_none,
+            ),
+            "Start a registered DTrace script",
+        )
+        self.add_metric(
+            "dtrace.control.stop",
+            pmdaMetric(
+                self.pmid(self.Control.CLUSTER, self.Control.STOP),
+                c_api.PM_TYPE_STRING,
+                c_api.PM_INDOM_NULL,
+                c_api.PM_SEM_INSTANT,
+                self._units_none,
+            ),
+            "Stop a running DTrace script",
+        )
+        self.add_metric(
+            "dtrace.control.reload",
+            pmdaMetric(
+                self.pmid(self.Control.CLUSTER, self.Control.RELOAD),
+                c_api.PM_TYPE_STRING,
+                c_api.PM_INDOM_NULL,
+                c_api.PM_SEM_INSTANT,
+                self._units_none,
+            ),
+            "Reload autostart scripts from disk",
+        )
+
+        self.add_metric(
+            "dtrace.scripts.state",
+            pmdaMetric(
+                self.pmid(self.Scripts.CLUSTER, self.Scripts.STATE),
+                c_api.PM_TYPE_STRING,
+                self.script_indom_id,
+                c_api.PM_SEM_DISCRETE,
+                self._units_none,
+            ),
+            "State of each registered DTrace script",
+        )
+        self.add_metric(
+            "dtrace.scripts.autostart",
+            pmdaMetric(
+                self.pmid(self.Scripts.CLUSTER, self.Scripts.AUTOSTART),
+                c_api.PM_TYPE_U32,
+                self.script_indom_id,
+                c_api.PM_SEM_DISCRETE,
+                self._units_none,
+            ),
+            "Whether the script autostarts on PMDA load",
+        )
+        self.add_metric(
+            "dtrace.scripts.last_error",
+            pmdaMetric(
+                self.pmid(self.Scripts.CLUSTER, self.Scripts.LAST_ERROR),
+                c_api.PM_TYPE_STRING,
+                self.script_indom_id,
+                c_api.PM_SEM_DISCRETE,
+                self._units_none,
+            ),
+            "Most recent error message for the script",
+        )
+        self.add_metric(
+            "dtrace.scripts.runtime_seconds",
+            pmdaMetric(
+                self.pmid(self.Scripts.CLUSTER, self.Scripts.RUNTIME),
+                c_api.PM_TYPE_U64,
+                self.script_indom_id,
+                c_api.PM_SEM_INSTANT,
+                self._units_seconds,
+            ),
+            "Seconds the script has been running",
+        )
+
+        self.set_fetch_callback(self._fetch_callback)
+        self.set_store_callback(self._store_callback)
+        self.set_attribute_callback(self._attribute_callback)
+        self.set_endcontext_callback(self._endcontext_callback)
+        self.set_user("root")
+
+        self._autostart_dir = (
+            Path(PCP.pmGetConfig("PCP_PMDAS_DIR")) / self.read_name() / "autostart.d"
+        )
+
+        atexit.register(self._shutdown_scripts)
+
+        if not self._in_pmda_setup():
+            # If running a newer pcp python binding that supports credentials,
+            # explicitly tell the C layer how to fall back if the server is old.
+            if cpmda and hasattr(cpmda, 'pmdaSetFlags'):
+                try:
+                    # PMDA_FLAG_CREDS_OPTIONAL allows the agent to handle
+                    # credentials if sent, but fall back seamlessly if an
+                    # older pmcd sends nothing.
+                    # PMDA_FLAG_CREDS_OPTIONAL constant value is typically 0x4
+                    cpmda.pmdaSetFlags(0x4)
+                except Exception:
+                    pass
+            try:
+                self.connect_pmcd()
+            except Exception as e:
+                sys.stderr.write(f"Connection failed: {str(e)}\n")
+                sys.exit(1)
+            self.reload_autostart()
+            self.pmda_ready()
+            self.log("Ready to process DTrace control requests.")
+
+    # ------------------------------------------------------------------
+    # Store callback handling control metrics
+    # ------------------------------------------------------------------
+    @staticmethod
+    def _load_control_config() -> Tuple[bool, bool, Set[str]]:
+        """Load the root-owned dynamic-script authorization policy."""
+        config = configparser.ConfigParser()
+        config_path = (
+            Path(PCP.pmGetConfig("PCP_PMDAS_DIR")) / "dtrace" / "dtrace.conf"
+        )
+
+        # Secure defaults: do not permit dynamically supplied programs.
+        enabled = False
+        auth_enabled = True
+        allowed_users: Set[str] = set()
+
+        try:
+            config.read(config_path)
+            if not config.has_section("dynamic_scripts"):
+                return enabled, auth_enabled, allowed_users
+
+            enabled = config.getboolean(
+                "dynamic_scripts", "enabled", fallback=enabled
+            )
+            auth_enabled = config.getboolean(
+                "dynamic_scripts", "auth_enabled", fallback=auth_enabled
+            )
+            configured_users = config.get(
+                "dynamic_scripts", "allowed_users", fallback=""
+            )
+        except (OSError, ValueError, configparser.Error):
+            return False, True, set()
+
+        allowed_users = {
+            user.strip() for user in configured_users.split(",") if user.strip()
+        }
+        return enabled, auth_enabled, allowed_users
+
+    def _attribute_callback(self, ctx: int, attr: int, value: str) -> None:
+        """Associate an authenticated client identity with its PCP context."""
+        if attr == cpmda.PMDA_ATTR_USERNAME:
+            with self._lock:
+                self._ctx_usernames[ctx] = value
+            return
+
+        if attr != cpmda.PMDA_ATTR_USERID:
+            return
+
+        try:
+            username = pwd.getpwuid(int(value)).pw_name
+        except (KeyError, OverflowError, ValueError):
+            # Do not retain an earlier, less trustworthy identity.
+            with self._lock:
+                self._ctx_usernames.pop(ctx, None)
+            return
+
+        with self._lock:
+            self._ctx_usernames[ctx] = username
+
+    def _endcontext_callback(self, ctx: int) -> None:
+        with self._lock:
+            self._ctx_usernames.pop(ctx, None)
+
+    def _control_store_permitted(self) -> bool:
+        if not self._dynamic_scripts_enabled:
+            return False
+        if not self._auth_enabled:
+            return True
+
+        try:
+            ctx = pmdaGetContext()
+        except Exception:  # pragma: no cover - PMDA library failure
+            return False
+
+        with self._lock:
+            username = self._ctx_usernames.get(ctx)
+        return username is not None and username in self._allowed_users
+
+    def _store_callback(self, cluster: int, item: int, inst: int, value: Any) -> int:
+        self.log(f"store callback")
+        if cluster != self.Control.CLUSTER:
+            return c_api.PM_ERR_PMID
+
+        if not self._control_store_permitted():
+            self.err("DTrace control store denied by dynamic-script policy")
+            return c_api.PM_ERR_PERMISSION
+
+        if isinstance(value, bytes):
+            text = value.decode("utf-8", errors="ignore")
+        else:
+            text = str(value)
+
+        if item == self.Control.REGISTER:
+            return self._handle_register(text)
+        if item == self.Control.UNREGISTER:
+            return self._handle_unregister(text)
+        if item == self.Control.START:
+            return self._handle_start(text)
+        if item == self.Control.STOP:
+            return self._handle_stop(text)
+        if item == self.Control.RELOAD:
+            self.reload_autostart()
+            return 0
+        return c_api.PM_ERR_PMID
+
+    def _handle_register(self, payload: str) -> int:
+        self.log(f"handle register {payload}")
+        try:
+            data = json.loads(payload)
+        except json.JSONDecodeError:
+            self.err("register payload must be valid JSON")
+            return c_api.PM_ERR_VALUE
+        if not isinstance(data, dict):
+            self.err("register payload must be a JSON object")
+            return c_api.PM_ERR_VALUE
+
+        name = data.get("name")
+        program = data.get("program")
+        if not name or not isinstance(name, str) or not program:
+            self.err("register payload requires string 'name' and 'program'")
+            return c_api.PM_ERR_VALUE
+
+        self.log(f"program is {program}")
+        options = data.get("options") if isinstance(data.get("options"), dict) else {}
+        separator = data.get("separator") if isinstance(data.get("separator"), str) else "."
+        autostart = bool(data.get("autostart", False))
+        pid, command = self._parse_target(data)
+        if pid is None and command is None and (
+            "pid" in data or "command" in data
+        ):
+            return c_api.PM_ERR_VALUE
+        defines = self._parse_defines(data.get("defines"))
+        if defines is None:
+            return c_api.PM_ERR_VALUE
+        compile_flags = self._parse_compile_flags(data.get("compile"))
+        if compile_flags is None:
+            return c_api.PM_ERR_VALUE
+        self.log(f"autostart is {autostart}")
+
+        with self._lock:
+            script = self._scripts.get(name)
+            if script:
+                script.autostart = autostart
+                script.separator = separator
+                script.update_definition(
+                    program, options, pid, command, defines, compile_flags
+                )
+            else:
+                script = ManagedDTraceScript(
+                    name, program, autostart, options, separator, pid, command,
+                    defines,
+                    compile_flags,
+                    self.log,
+                )
+                self._scripts[name] = script
+                self._assign_instance(name)
+            script.register_data_callbacks(self)
+
+        if autostart:
+            self.log(f"starting script {name}...")
+            ok, error = script.start()
+            if not ok:
+                self.err(f"failed to start script {name}: {error}")
+            self.log(f"script {name} started ok")
+        return 0
+
+    def _parse_target(
+        self, data: Dict[str, Any]
+    ) -> Tuple[Optional[int], Optional[List[str]]]:
+        """Validate JSON equivalents of dtrace(1)'s -p and -c options."""
+        pid = data.get("pid")
+        command = data.get("command")
+
+        if pid is not None and command is not None:
+            self.err("register payload must specify only one of 'pid' or 'command'")
+            return None, None
+
+        if pid is not None:
+            if isinstance(pid, bool) or not isinstance(pid, int) or pid <= 0:
+                self.err("register payload 'pid' must be a positive integer")
+                return None, None
+            if pid > 0x7fffffff:
+                self.err("register payload 'pid' is out of range")
+                return None, None
+            return pid, None
+
+        if command is not None:
+            if not isinstance(command, str):
+                self.err("register payload 'command' must be a non-empty string")
+                return None, None
+            try:
+                argv = shlex.split(command)
+            except ValueError:
+                self.err("register payload 'command' has invalid shell quoting")
+                return None, None
+            if not argv:
+                self.err("register payload 'command' must be a non-empty string")
+                return None, None
+            return None, argv
+
+        return None, None
+
+    def _parse_defines(self, value: Any) -> Optional[List[str]]:
+        """Validate macro definitions and return NAME[=VALUE] strings."""
+        if value is None:
+            return []
+        items = value.items() if isinstance(value, dict) else value
+        if not isinstance(value, (dict, list)):
+            self.err("register payload 'defines' must be an object or array")
+            return None
+        result = []
+        for item in items:
+            if isinstance(value, dict):
+                name, macro_value = item
+                if not isinstance(name, str) or not re.fullmatch(
+                    r"[A-Za-z_][A-Za-z0-9_]*", name
+                ):
+                    self.err("register payload has invalid macro name")
+                    return None
+                result.append(name if macro_value is None else f"{name}={macro_value}")
+            elif not isinstance(item, str) or not re.fullmatch(
+                r"[A-Za-z_][A-Za-z0-9_]*(=.*)?", item
+            ):
+                self.err(
+                    "register payload 'defines' entries must be NAME or NAME=VALUE"
+                )
+                return None
+            else:
+                result.append(item)
+        return result
+
+    def _parse_compile_flags(self, value: Any) -> Optional[int]:
+        """Translate supported compile-time flags to libdtrace cflags."""
+        if value is None:
+            return 0
+        if not isinstance(value, dict):
+            self.err("register payload 'compile' must be an object")
+            return None
+        flags = 0
+        if value.get("zdefs", False):
+            flags |= c_api.DTRACE_C_ZDEFS
+        unknown = set(value) - {"zdefs"}
+        if unknown:
+            self.err("unsupported compile option(s): " + ", ".join(sorted(unknown)))
+            return None
+        return flags
+
+    def _handle_unregister(self, name: str) -> int:
+        name = name.strip()
+        if not name:
+            return c_api.PM_ERR_VALUE
+        script = None
+        with self._lock:
+            script = self._scripts.pop(name, None)
+            if script is None:
+                return c_api.PM_ERR_NAME
+            self._rebuild_instances_locked()
+        if script is not None:
+            if script._state != "stopped":
+                script.stop()
+            script.unregister_data_callbacks(self)
+        return 0
+
+    def _handle_start(self, name: str) -> int:
+        self.log(f"handle start {name}")
+        name = name.strip()
+        if not name:
+            return c_api.PM_ERR_VALUE
+        script = self._scripts.get(name)
+        if script is None:
+            return c_api.PM_ERR_NAME
+        ok, error = script.start()
+        if not ok:
+            self.err(f"start failed for {name}: {error}")
+            return c_api.PM_ERR_GENERIC
+        return 0
+
+    def _handle_stop(self, name: str) -> int:
+        self.log(f"handle stop {name}")
+        name = name.strip()
+        if not name:
+            return c_api.PM_ERR_VALUE
+        script = self._scripts.get(name)
+        if script is None:
+            return c_api.PM_ERR_NAME
+        script.stop()
+        return 0
+
+    # ------------------------------------------------------------------
+    # Fetch callback serving metric values
+    # ------------------------------------------------------------------
+    def _fetch_callback(self, cluster: int, item: int, inst: int):
+        if cluster > self.Scripts.CLUSTER:
+            ids = (cluster, item)
+            if ids not in self._data_metric_ids:
+                return [cpmda.PMDA_FETCH_NOVALUES, 0]
+            metric = self._data_metric_ids[(cluster, item)]
+            if inst not in metric._data_instance_ids:
+                return [cpmda.PMDA_FETCH_NOVALUES, 0]
+            instance = metric._data_instance_ids[inst]
+            value = instance._value
+            if value is None:
+                return [cpmda.PMDA_FETCH_NOVALUES, 0]
+            return [value, 1]
+        if cluster == self.Control.CLUSTER:
+            # Control metrics are write-only, but returning NOVALUES causes
+            # clients like pmstore to abort before issuing the store PDU.
+            # Hand back an empty string so the store callback still fires.
+            return ["", 1]
+
+        if cluster != self.Scripts.CLUSTER:
+            return [c_api.PM_ERR_PMID, 0]
+
+        name = self._inst_map.get(inst)
+        if not name:
+            return [c_api.PM_ERR_INST, 0]
+        script = self._scripts.get(name)
+        if not script:
+            return [c_api.PM_ERR_INST, 0]
+
+        if item == self.Scripts.STATE:
+            return [script.state(), 1]
+        if item == self.Scripts.AUTOSTART:
+            return [1 if script.autostart else 0, 1]
+        if item == self.Scripts.LAST_ERROR:
+            return [script.last_error(), 1]
+        if item == self.Scripts.RUNTIME:
+            return [script.runtime_seconds(), 1]
+
+        return [c_api.PM_ERR_PMID, 0]
+
+    # ------------------------------------------------------------------
+    # Autostart support
+    # ------------------------------------------------------------------
+    def reload_autostart(self) -> None:
+        if not self._autostart_dir.is_dir():
+            return
+
+        autostart_names = set()
+        for script_path in sorted(self._autostart_dir.glob("*.d")):
+            try:
+                program = script_path.read_text()
+            except OSError as exc:  # pragma: no cover - filesystem error
+                self.err(f"unable to read {script_path}: {exc}")
+                continue
+
+            metadata = self._load_metadata(script_path)
+            name = metadata.get("name") or script_path.stem
+            options = metadata.get("options") if isinstance(metadata.get("options"), dict) else {}
+            separator = metadata.get("separator") if isinstance(metadata.get("separator"), str) else "."
+            pid, command = self._parse_target(metadata)
+            if pid is None and command is None and (
+                "pid" in metadata or "command" in metadata
+            ):
+                self.err(f"invalid target in autostart metadata {script_path}")
+                continue
+            defines = self._parse_defines(metadata.get("defines"))
+            if defines is None:
+                self.err(f"invalid macro definitions in {script_path}")
+                continue
+            compile_flags = self._parse_compile_flags(metadata.get("compile"))
+            if compile_flags is None:
+                self.err(f"invalid compile options in {script_path}")
+                continue
+
+            autostart_names.add(name)
+
+            with self._lock:
+                script = self._scripts.get(name)
+                if script:
+                    script.autostart = True
+                    script.separator = separator
+                    script.update_definition(
+                        program, options, pid, command, defines, compile_flags
+                    )
+                else:
+                    script = ManagedDTraceScript(
+                        name, program, True, options, separator, pid, command,
+                        defines,
+                        compile_flags,
+                        self.log,
+                    )
+                    self._scripts[name] = script
+                    self._assign_instance(name)
+                    script.register_data_callbacks(self)
+
+            ok, error = script.start()
+            if not ok:
+                self.err(f"autostart of {name} failed: {error}")
+
+        with self._lock:
+            for name, script in self._scripts.items():
+                script.autostart = name in autostart_names or script.autostart
+
+    def _load_metadata(self, script_path: Path) -> Dict[str, Any]:
+        self.log("load metadata")
+        meta_path = script_path.with_suffix(".json")
+        if not meta_path.is_file():
+            return {}
+        try:
+            return json.loads(meta_path.read_text())
+        except (OSError, json.JSONDecodeError) as exc:  # pragma: no cover
+            self.err(f"failed to parse metadata {meta_path}: {exc}")
+            return {}
+
+    # ------------------------------------------------------------------
+    # Instance domain management
+    # ------------------------------------------------------------------
+    def _assign_instance(self, name: str) -> None:
+        self.log("assign instance")
+        with self._lock:
+            inst_id = self._next_inst
+            self._next_inst += 1
+            self._inst_map[inst_id] = name
+            self._rebuild_indom()
+
+    def _rebuild_instances_locked(self) -> None:
+        # assumes caller holds self._lock
+        self._inst_map = {
+            idx: name for idx, name in enumerate(sorted(self._scripts.keys()))
+        }
+        self._next_inst = len(self._inst_map)
+        self._rebuild_indom()
+
+    def _rebuild_data_indom(self) -> None:
+        with self._lock:
+            for m in self._data_metrics.values():
+                self.replace_indom(m.indom_id, m._data_indom_insts)
+
+
+    def _rebuild_indom(self) -> None:
+        insts = [pmdaInstid(inst, name) for inst, name in sorted(self._inst_map.items())]
+        self.replace_indom(self.script_indom_id, insts)
+
+    # ------------------------------------------------------------------
+    # Teardown helpers
+    # ------------------------------------------------------------------
+    def _shutdown_scripts(self) -> None:
+        with self._lock:
+            scripts = list(self._scripts.values())
+        for script in scripts:
+            script.unregister_data_callbacks(self)
+            script.stop()
+
+    # ------------------------------------------------------------------
+    @staticmethod
+    def _in_pmda_setup() -> bool:
+        return bool(os.environ.get("PCP_PYTHON_DOMAIN") or os.environ.get("PCP_PYTHON_PMNS"))
+
+
+def main() -> None:
+    domain = -1
+    with open(os.path.dirname(os.path.abspath(__file__)) + "/Install") as f:
+        for line in f:
+            if "domain=" in line:
+                 domain = int(line.split("=")[1].strip())
+    if domain == -1 :
+        print("No domain value in Install, exiting.")
+    else:
+        DTracePMDA("dtrace", 487).run()
+
+
+if __name__ == "__main__":
+    main()
-- 
2.43.5




More information about the DTrace-devel mailing list