From f1aafd154928e88416832254c127fc3895042014 Mon Sep 17 00:00:00 2001 From: keithlostracco Date: Sun, 26 Jul 2026 15:24:34 -0700 Subject: [PATCH] Prepare 0.4.0 for public release Fix a null-pointer crash on the event loop's initialization failure path. execute() reported the failure by calling strcmp on m_warning, but only one of the seven failure paths in initializeAsyncio() sets it -- the rest set m_error and leave m_warning null. Any real failure, such as asyncio failing to import, dereferenced null inside TouchDesigner instead of showing the error. Those paths already log themselves, so only the remaining one needs surfacing here. Also fix the empty CFBundleName in the macOS bundle (Info.plist.in substitutes MACOSX_BUNDLE_BUNDLE_NAME, which CMake was never given) and set the project version to 0.4.0, so the bundle stops reporting a 1.0.0 that corresponds to no release. Cut releases by pushing a v*.*.* tag rather than by publishing in the GitHub UI. The workflow now builds, tests, and packages on both platforms before anything is published -- the release build previously ran no tests at all -- refuses a tag that is not an ancestor of main, and takes its notes from the CHANGELOG entry for the tag. Both workflows declare least-privilege permissions and pin the same toolchain as the other repositories. Drop the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 workaround; that cutover has passed. Add the material a public repository needs: a changelog, issue forms, a pull request template, and Dependabot coverage for the workflow actions and the pytest requirements. Document installing a release build without compiling, including clearing the quarantine attribute on macOS, and document the plugin registry API, which was exposed but never written up. Correct the debug sections in README.md and TESTING.md, which described an output that does not exist and a parameter under the wrong name. --- .github/ISSUE_TEMPLATE/bug_report.yml | 94 ++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 37 ++++++ .github/PULL_REQUEST_TEMPLATE.md | 42 +++++++ .github/dependabot.yml | 29 +++++ .github/workflows/ci.yml | 56 ++++----- .github/workflows/release.yml | 136 +++++++++++++++++---- CHANGELOG.md | 104 ++++++++++++++++ CMakeLists.txt | 3 +- CONTRIBUTING.md | 32 ++++- README.md | 135 +++++++++++++++++--- SECURITY.md | 72 ++++++++--- TESTING.md | 8 +- src/asyncio_dat.cpp | 6 +- 14 files changed, 659 insertions(+), 103 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 CHANGELOG.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..6790161 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,94 @@ +name: Bug report +description: The operator behaves incorrectly in TouchDesigner +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to file a report. If this is a **security** + issue, please use private reporting instead — see [SECURITY.md](../blob/main/SECURITY.md). + + - type: textarea + id: summary + attributes: + label: What happened? + description: What did you expect, and what did you get instead? + placeholder: | + Tasks added with add_task() stop running after the Reset pulse, but I expected ... + validations: + required: true + + - type: textarea + id: repro + attributes: + label: Reproduction + description: > + The smallest script that shows the problem, run against an AsyncioDAT + operator. It will be rendered as Python automatically. If it needs a + `.toe` to reproduce, attach a minimal one as well. + render: python + placeholder: | + import asyncio + + adat = op('Asyncio1') + + async def repro(): + await asyncio.sleep(1.0) + print("done") + + adat.add_task(repro()) + validations: + required: true + + - type: textarea + id: status + attributes: + label: Operator status output + description: > + The rows from the AsyncioDAT operator's status table, plus anything + relevant from the textport. + validations: + required: false + + - type: input + id: version + attributes: + label: AsyncioDAT version + description: Release tag, or the commit SHA you built from. + placeholder: "v0.4.0, or 1a2b3c4" + validations: + required: true + + - type: input + id: td_version + attributes: + label: TouchDesigner version + placeholder: "2025.32820" + validations: + required: true + + - type: input + id: environment + attributes: + label: Platform + placeholder: "Windows 11, or macOS 15 (Apple Silicon)" + validations: + required: true + + - type: dropdown + id: origin + attributes: + label: How did you get the operator? + options: + - Downloaded a release asset + - Built from source + validations: + required: true + + - type: textarea + id: notes + attributes: + label: Anything else? + description: Stack traces, crash dumps, or a failing test case. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..665d08b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Usage, parameters, and the Python API + url: https://github.com/Actualize-Interactive/AsyncioDAT#readme + about: The README covers installation, every parameter, and the full Python API — worth checking before filing. + - name: Report a security vulnerability + url: https://github.com/Actualize-Interactive/AsyncioDAT/security/advisories/new + about: Please report security issues privately, not as a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..62c6428 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,37 @@ +name: Feature request +description: Suggest a capability, parameter, or API addition +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: What problem would this solve? + description: > + Describe the use case rather than the implementation. What are you + trying to do in TouchDesigner that the operator makes hard or + impossible today? + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: Proposed API + description: > + If you have a shape in mind, sketch it — a Python method, a getset, an + operator parameter, or a callback. Additive changes are much easier to + accept than changes to existing signatures or parameter names, which + break saved `.toe` files. + render: python + validations: + required: false + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: > + Anything you tried, or workarounds you are using now — including + whether it can already be done through `get_event_loop()`. + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..d7c0b8b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,42 @@ + + +## What does this change? + + + +## Why? + + + +## How was it tested? + + + +## Checklist + +- [ ] The project builds on my platform (`.\build.ps1` or `./build.sh`). +- [ ] The unit suites pass (`cmake --workflow --preset dev`). +- [ ] The TouchDesigner integration suite passes, or is not affected + (`.\run_td_tests.ps1` / `./run_td_tests.sh`). +- [ ] New or changed behavior is covered by tests. +- [ ] `README.md` / `TESTING.md` updated for any change to parameters, the + Python API, or the callbacks. +- [ ] `CHANGELOG.md` has an entry under `[Unreleased]`, if user-visible. + +## Breaking changes + + + +None diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2b4152d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,29 @@ +version: 2 + +updates: + # Keep the workflow actions current. The only package-manager manifest in the + # repository is the pytest requirements file below: the C++ dependency + # (toml11) is pinned as a FetchContent git tag in CMakeLists.txt, which + # Dependabot cannot parse, so it is bumped by hand. + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + commit-message: + prefix: "ci" + groups: + github-actions: + patterns: + - "*" + + # Test-only Python dependencies (pytest and friends). + - package-ecosystem: "pip" + directory: "/tests/python" + schedule: + interval: "monthly" + commit-message: + prefix: "test" + groups: + pytest: + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53b4a15..4adcf02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,73 +5,67 @@ on: branches: [ main ] pull_request: branches: [ main ] + workflow_dispatch: -# Some third-party JS actions (e.g. setup-cmake) still ship on Node.js 20; opt -# their JS actions into Node 24 ahead of the forced cutover (2026-06-02). -# Remove once all actions ship Node 24. -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true +permissions: + contents: read jobs: build: + runs-on: ${{ matrix.os }} + timeout-minutes: 20 strategy: - fail-fast: false matrix: os: [windows-latest, macos-latest] - build_type: [Release] - - runs-on: ${{ matrix.os }} + fail-fast: false steps: - - name: Checkout code - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - - name: Setup CMake - uses: jwlawson/actions-setup-cmake@v2 + - name: Setup cmake + uses: lukka/get-cmake@v4.4.0 with: - cmake-version: '3.25' + cmakeVersion: '4.4.0' # TouchDesigner embeds CPython 3.11, so we build against 3.11. setup-python - # provides the headers + import library that CMake's Development.Module - # component needs (Python itself is not vendored in the repo). + # provides the headers and import library that CMake's Development.Module + # component needs (Python itself is not vendored in the repository). - name: Setup Python 3.11 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.11' - name: Configure CMake run: > - cmake -B build - -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + cmake -B build -S . + -DCMAKE_BUILD_TYPE=Release -DPython3_ROOT_DIR="${{ env.pythonLocation }}" -DASYNCIODAT_BUILD_TESTS=ON - - name: Build project - run: cmake --build build --config ${{ matrix.build_type }} - - - name: List build outputs - shell: bash - run: ls -R build/bin + - name: Build + run: cmake --build build --config Release --parallel 2 - # Unit tests (Catch2 + pytest) — no TouchDesigner required. + # tests/cpp (Catch2) and tests/python (pytest) only — the tests/td + # integration suite needs a licensed TouchDesigner and a GPU, so it runs + # locally via run_td_tests.ps1 / run_td_tests.sh instead. - name: Install test dependencies run: python -m pip install --upgrade pip -r tests/python/requirements.txt - - name: Run unit tests - run: ctest --test-dir build -C ${{ matrix.build_type }} --output-on-failure + - name: Test + run: ctest --test-dir build -C Release --output-on-failure - name: Upload build artifact (Windows) if: runner.os == 'Windows' uses: actions/upload-artifact@v7 with: - name: AsyncioDAT-windows-${{ matrix.build_type }} - path: build/bin/${{ matrix.build_type }}/AsyncioDAT.dll + name: AsyncioDAT-windows + path: build/bin/Release/AsyncioDAT.dll if-no-files-found: error - name: Upload build artifact (macOS) if: runner.os == 'macOS' uses: actions/upload-artifact@v7 with: - name: AsyncioDAT-macos-${{ matrix.build_type }} + name: AsyncioDAT-macos path: build/bin/AsyncioDAT.plugin if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 58bc7ed..cbdd879 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,60 +1,146 @@ name: Release on: - release: - types: [published] + push: + tags: + - 'v*.*.*' + workflow_dispatch: -# Some third-party JS actions (e.g. setup-cmake, action-gh-release) still ship -# on Node.js 20; opt their JS actions into Node 24 ahead of the forced cutover -# (2026-06-02). Remove once all actions ship Node 24. -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true +permissions: + contents: read jobs: - build-and-release: + # Build, test, and package on every platform as the release gate. Nothing is + # published unless this passes on both. + build: strategy: - fail-fast: false matrix: os: [windows-latest, macos-latest] - + fail-fast: false runs-on: ${{ matrix.os }} + timeout-minutes: 20 steps: - - name: Checkout code - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - - name: Setup CMake - uses: jwlawson/actions-setup-cmake@v2 + - name: Setup cmake + uses: lukka/get-cmake@v4.4.0 with: - cmake-version: '3.25' + cmakeVersion: '4.4.0' - name: Setup Python 3.11 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.11' - name: Configure CMake run: > - cmake -B build + cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DPython3_ROOT_DIR="${{ env.pythonLocation }}" + -DASYNCIODAT_BUILD_TESTS=ON + + - name: Build + run: cmake --build build --config Release --parallel 2 - - name: Build project - run: cmake --build build --config Release + - name: Install test dependencies + run: python -m pip install --upgrade pip -r tests/python/requirements.txt + + - name: Test + run: ctest --test-dir build -C Release --output-on-failure - # Package the platform artifact into a single uploadable file. - name: Package (Windows) if: runner.os == 'Windows' shell: pwsh run: Compress-Archive -Path build/bin/Release/AsyncioDAT.dll -DestinationPath AsyncioDAT-windows.zip + # ditto, not zip: the .plugin is a bundle, and ditto preserves the symlinks + # and resource forks a plain zip would flatten. - name: Package (macOS) if: runner.os == 'macOS' run: ditto -c -k --keepParent build/bin/AsyncioDAT.plugin AsyncioDAT-macos.zip - - name: Upload Release Asset - uses: softprops/action-gh-release@v2 + - name: Upload package + uses: actions/upload-artifact@v7 + with: + name: package-${{ runner.os }} + path: AsyncioDAT-*.zip + if-no-files-found: error + + # Publish the release from the artifacts the gate produced. + # + # Only runs for tag pushes. A release cannot be published from a branch, so on + # workflow_dispatch this job is skipped and the workflow acts as a dry run of + # the cross-platform build, test, and packaging gate above. + release: + if: startsWith(github.ref, 'refs/tags/') + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + # Squash-merging a pull request rewrites the commit, so a tag pushed to the + # pre-merge branch tip builds and tests green while pointing at a commit + # reachable only from the tag. Publishing that produces a release whose + # history is not on main, which is only noticed much later by whoever pins + # it. Refuse the tag instead. + - name: Verify the tag is on main + run: | + git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main + if ! git merge-base --is-ancestor "$GITHUB_SHA" refs/remotes/origin/main; then + echo "::error::$GITHUB_REF_NAME ($GITHUB_SHA) is not an ancestor of main. Delete the tag, then re-tag the merged commit on main." >&2 + exit 1 + fi + echo "$GITHUB_REF_NAME is on main." + + - name: Download Windows package + uses: actions/download-artifact@v8 + with: + name: package-Windows + path: dist + + - name: Download macOS package + uses: actions/download-artifact@v8 + with: + name: package-macOS + path: dist + + # Use the CHANGELOG entry for this tag as the release body, so the notes are + # written once and reviewed in the pull request that introduced them. + - name: Extract release notes from CHANGELOG + run: | + version="${GITHUB_REF_NAME#v}" + # Match the heading by prefix rather than by regex: a version string + # interpolated into a regex turns "[0.4.0]" into a character class, and + # whether the escaping survives depends on the awk in use (mawk keeps + # the backslash, gawk drops it and silently matches nothing). + awk -v head="## [$version]" ' + index($0, head) == 1 { found = 1; next } # heading for this version + found && index($0, "## [") == 1 { exit } # next version heading + found && /^\[.*\]: / { exit } # link reference block + found && index($0, " + +[Unreleased]: https://github.com/Actualize-Interactive/AsyncioDAT/compare/v0.4.0...HEAD +[0.4.0]: https://github.com/Actualize-Interactive/AsyncioDAT/compare/v0.3.0...v0.4.0 +[0.3.0]: https://github.com/Actualize-Interactive/AsyncioDAT/compare/v0.2.2...v0.3.0 +[0.2.2]: https://github.com/Actualize-Interactive/AsyncioDAT/compare/v0.2.1...v0.2.2 +[0.2.1]: https://github.com/Actualize-Interactive/AsyncioDAT/compare/v0.2.0...v0.2.1 +[0.2.0]: https://github.com/Actualize-Interactive/AsyncioDAT/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/Actualize-Interactive/AsyncioDAT/releases/tag/v0.1.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 37427e2..8c15a94 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.25) -project(AsyncioDAT VERSION 1.0.0 LANGUAGES CXX) +project(AsyncioDAT VERSION 0.4.0 LANGUAGES CXX) set(VERBOSE_STATUS OFF CACHE BOOL "Verbose status messages" FORCE) set(SUPPRESS_NOT_REFERENCED_WARNINGS ON CACHE BOOL "Suppress not referenced warnings" FORCE) @@ -165,6 +165,7 @@ elseif(APPLE) BUNDLE TRUE BUNDLE_EXTENSION "plugin" MACOSX_BUNDLE_GUI_IDENTIFIER "com.actualize.asyncio-dat" + MACOSX_BUNDLE_BUNDLE_NAME "AsyncioDAT" MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION} MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 821dfe1..706c193 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,13 +5,16 @@ bug reports, fixes, docs, and features — are welcome. ## Reporting issues -Open a GitHub issue and include: +Open a GitHub issue using the bug report form, which asks for: - Your TouchDesigner version (e.g. 2025.32820) and OS. - What you expected to happen vs. what happened. - A minimal `.toe` or script that reproduces the problem, if possible. - Relevant output from the AsyncioDAT operator's status table / textport. +Security issues go through [private reporting](SECURITY.md) instead, not a +public issue. + ## Development setup AsyncioDAT is a C++ TouchDesigner Custom Operator (a DAT) that embeds an @@ -65,11 +68,38 @@ changed is covered by or verified against the test scripts before opening a PR. ## Pull requests - Branch off `main` and keep PRs focused. +- Use clear, imperative commit messages with a type prefix (`fix:`, `feat:`, + `docs:`, `test:`, `chore:`, `ci:`). - Match the existing code style (tabs in C++ sources, existing naming). - Update `README.md` / `TESTING.md` when you change behavior, parameters, or the Python API. +- Add an entry under `[Unreleased]` in [CHANGELOG.md](CHANGELOG.md) for any + user-visible change, and call out breaking changes explicitly. - Describe how you tested the change. +**Operator parameter names and the Python API are stable.** A parameter's +internal name (`Autopoll`, `Maxstatusrows`, …) is what saved `.toe` files store, +so renaming one silently drops the user's setting on load. Prefer additive +changes; if a break is unavoidable, say so in the PR and in the changelog. + +## Releasing + +Releases are cut from `main` by pushing a tag: + +1. In a PR, bump `project(AsyncioDAT VERSION …)` in `CMakeLists.txt` and move + the `[Unreleased]` changelog entries under a `## [x.y.z] - YYYY-MM-DD` + heading, adding the compare link at the bottom of the file. +2. After it merges, tag the merge commit on `main` and push the tag: + + ```bash + git tag vx.y.z && git push origin vx.y.z + ``` + +The Release workflow builds, tests, and packages on Windows and macOS, refuses +the tag if it is not an ancestor of `main`, and publishes the release using the +changelog entry for that version as the release notes. It fails if no such entry +exists. + ## License By contributing, you agree that your contributions are licensed under the diff --git a/README.md b/README.md index c4787d1..8c09050 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # AsyncioDAT - Asyncio Event Loop for TouchDesigner +[![CI](https://github.com/Actualize-Interactive/AsyncioDAT/actions/workflows/ci.yml/badge.svg)](https://github.com/Actualize-Interactive/AsyncioDAT/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +![C++20](https://img.shields.io/badge/C%2B%2B-20-blue.svg) +![TouchDesigner 2025.32820](https://img.shields.io/badge/TouchDesigner-2025.32820-orange.svg) + AsyncioDAT is a C++ TouchDesigner operator that provides a managed asyncio event loop for Python scripting within TouchDesigner. It enables truly asynchronous programming without blocking the main TouchDesigner thread. ## Features @@ -10,16 +15,56 @@ AsyncioDAT is a C++ TouchDesigner operator that provides a managed asyncio event - **Easy Python Integration**: Simple Python API for adding coroutines and managing async tasks - **Comprehensive Error Handling**: Robust error handling and recovery mechanisms +## Requirements + +- **TouchDesigner 2025.32820** or newer. The operator is built against Custom + Operator SDK v4 and TouchDesigner's embedded CPython **3.11**. +- **Windows 10/11**, or **macOS on Apple Silicon**. The macOS build is arm64 + only, matching TouchDesigner. + ## Installation -1. Build the project using the provided PowerShell script: - ```powershell - .\build.ps1 +### From a release (no build required) + +1. Download the asset for your platform from the + [latest release](https://github.com/Actualize-Interactive/AsyncioDAT/releases/latest) + and extract it: + + | Platform | Asset | Contents | + | --- | --- | --- | + | Windows | `AsyncioDAT-windows.zip` | `AsyncioDAT.dll` | + | macOS | `AsyncioDAT-macos.zip` | `AsyncioDAT.plugin` bundle | + +2. Put the operator in a **`Plugins/` folder beside your `.toe`**. TouchDesigner + loads Custom Operators from a `Plugins/` directory next to the project file; + there is no way to point it at an arbitrary directory. (A `Plugins/` folder + in your TouchDesigner user directory — `Documents/Derivative/Plugins/` — + works too, and makes the operator available to every project.) + + ```text + MyProject/ + ├── MyProject.toe + └── Plugins/ + └── AsyncioDAT.dll # or AsyncioDAT.plugin on macOS ``` -2. The DLL will be automatically copied to the `tests/td/Plugins/` directory +3. **macOS only:** the downloaded bundle carries a quarantine attribute and is + not signed or notarized, so TouchDesigner will refuse to load it until you + clear the attribute: -3. Open the test TouchDesigner file: `tests/td/test.toe` + ```bash + xattr -dr com.apple.quarantine Plugins/AsyncioDAT.plugin + ``` + +4. Open your project. On first load, TouchDesigner shows a modal asking you to + **approve/trust** the Custom Operator — accept it. Then add an **Asyncio** + DAT from the operator palette. + +### From source + +See [Building from Source](#building-from-source) below. `build.ps1` / +`build.sh` copy the freshly built operator into `tests/td/Plugins/`, so the +bundled test project (`tests/td/test.toe`) picks it up. ## Configuration @@ -113,8 +158,48 @@ loop = asyncio_op.get_event_loop() # Use the loop directly for advanced operations if loop: loop.call_later(5.0, lambda: print("Called after 5 seconds")) + +# Number of callbacks currently ready to run on the loop +pending = asyncio_op.get_callback_count() ``` +#### Plugin Registry + +The operator holds a dictionary of named Python objects that outlives any single +script — useful for keeping a client, a connection pool, or a service object +alive across cooks. Register them from `on_start` (see +[Lifecycle Callbacks](#lifecycle-callbacks)) and reach them from anywhere. + +```python +asyncio_op = op('Asyncio1') + +# Register / replace. Returns True on success; re-registering a name replaces +# the previous object and releases the reference to it. +asyncio_op.set_plugin('my_service', MyService()) + +# Look up. Returns None if the name is not registered. +service = asyncio_op.get_plugin('my_service') + +asyncio_op.has_plugin('my_service') # -> True +asyncio_op.del_plugin('my_service') # -> True, or False if it wasn't there +asyncio_op.clear_plugins() # remove all of them +``` + +The `plugins` property is an attribute-style view over the same registry, and +`plugin_names` lists the registered names: + +```python +asyncio_op.plugins.my_service = MyService() # same as set_plugin +service = asyncio_op.plugins.my_service # AttributeError if not registered +del asyncio_op.plugins.my_service # same as del_plugin + +'my_service' in asyncio_op.plugins # -> bool +asyncio_op.plugin_names # -> list of names +``` + +These methods act on whichever AsyncioDAT instance owns the event loop, so +calling them before one is active raises `RuntimeError`. + ### Example Usage #### Simple Async Task @@ -290,7 +375,7 @@ async def robust_task(): - Try using the Reset pulse parameter 2. **Tasks Not Executing** - - Verify "Auto Process Events" is enabled + - Verify "Auto Poll" is enabled - Check the operator's status output for error messages - Ensure the operator is cooking every frame @@ -306,12 +391,16 @@ async def robust_task(): ### Debug Information -The operator's output provides real-time status: -- Execute count (frames processed) -- Asyncio initialization status -- Event loop running state -- Auto-processing state -- Available methods list +The operator's output table carries the most recent status messages (how many is +set by **Max Status Rows**), and the same messages go to the textport. Attach an +Info CHOP to the operator for live counters: + +| Channel | Meaning | +| --- | --- | +| `event_loop_active` | 1 while asyncio is initialized | +| `event_loop_auto_poll` | 1 while **Auto Poll** is on | +| `event_loop_poll_count` | Frames polled since initialization | +| `event_loop_poll_duration` | Time spent in the last poll, in milliseconds | ## Building from Source @@ -346,18 +435,23 @@ headers are included under `ext/td/include/` (see [NOTICE](NOTICE)). ### Continuous Integration The project includes GitHub Actions workflows for: -- **CI**: Automatic builds on Windows and macOS for every push and pull request to `main` -- **Release**: Automatic building and publishing of artifacts when a release is published +- **CI**: builds and runs the unit suites on Windows and macOS for every push and + pull request to `main` +- **Release**: triggered by pushing a `v*.*.*` tag. It builds, tests, and + packages on both platforms before publishing anything, and takes the release + notes from the matching [CHANGELOG.md](CHANGELOG.md) entry. See + [CONTRIBUTING.md](CONTRIBUTING.md#releasing). Release artifacts (`AsyncioDAT-windows.zip` containing `AsyncioDAT.dll`, and -`AsyncioDAT-macos.zip` containing the `AsyncioDAT.plugin` bundle) are built and -attached to GitHub releases for easy download. +`AsyncioDAT-macos.zip` containing the `AsyncioDAT.plugin` bundle) are attached +to GitHub releases for easy download. ### Development The project structure: - `src/asyncio_dat.cpp/h`: Main operator implementation - `src/py_bindings.cpp/h`: Python C API bindings +- `src/config.cpp/h`: `config.toml` parsing - `ext/td/include/`: TouchDesigner Custom Operator SDK headers (see [NOTICE](NOTICE)) - `tests/cpp/`: Catch2 unit tests (no TouchDesigner required) - `tests/python/`: pytest suite against a compiled test extension (no TouchDesigner required) @@ -366,11 +460,16 @@ The project structure: ## License -This project is licensed under the MIT License. See LICENSE file for details. +This project is licensed under the MIT License — see [LICENSE](LICENSE). Bundled +and third-party components are attributed in [NOTICE](NOTICE). ## Contributing -Contributions are welcome! Please feel free to submit pull requests or open issues for bugs and feature requests. +Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the build, +test, and pull request expectations. Security issues go through +[private reporting](SECURITY.md), not a public issue. + +Release-by-release changes are recorded in [CHANGELOG.md](CHANGELOG.md). ## Support diff --git a/SECURITY.md b/SECURITY.md index 6aaace7..9ebe8a4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,24 +2,60 @@ ## Supported versions -AsyncioDAT is maintained on a rolling basis. Security fixes target the latest -release and the `main` branch. +AsyncioDAT is pre-1.0 and is developed on a single line. Security fixes are +applied to the latest release only; there are no maintained backport branches. -## Reporting a vulnerability - -Please report security issues privately rather than opening a public issue. - -- Use GitHub's **"Report a vulnerability"** (Security → Advisories) on this - repository, or -- Email **keith@actualize.vision** with details and reproduction steps. +| Version | Supported | +| --- | --- | +| 0.4.x | ✅ | +| < 0.4 | ❌ | -We will acknowledge your report and keep you updated on remediation. Thank you -for helping keep AsyncioDAT and its users safe. - -## Scope notes +## Reporting a vulnerability -AsyncioDAT runs Python coroutines inside TouchDesigner's interpreter and can -load callback modules from disk (see the `config.toml` `callback_module_path` -and `[main].paths` settings). Treat `.toe` files and any configured callback -modules / `sys.path` entries as trusted code: a malicious project file can run -arbitrary Python, just as a native TouchDesigner project can. +**Please do not report security issues through public GitHub issues.** + +Report privately through GitHub's +[private vulnerability reporting](https://github.com/Actualize-Interactive/AsyncioDAT/security/advisories/new). +The form is the only reporting channel; it lets us discuss and fix the issue +with you before anything becomes public, and it requires nothing more than a +GitHub account. + +Please include: + +- the affected version or commit, and your TouchDesigner version, +- a description of the issue and its impact, +- the steps, script, or minimal `.toe` needed to reproduce it, +- and any suggested fix, if you have one. + +You can expect an acknowledgement within a few business days. We will keep you +informed as we investigate, and will credit you in the release notes when the +fix ships unless you prefer otherwise. + +## Scope + +AsyncioDAT is a native operator loaded into TouchDesigner's process. It has no +network or process boundary of its own: it drives an asyncio event loop on +behalf of the Python code in the hosting project. The issues most relevant here +are memory-safety and lifetime problems in the operator itself, such as: + +- crashes or memory corruption reachable from the Python API (`add_task`, + `create_task`, `run_coroutine`, the plugin registry) with unusual but + legitimate arguments, +- reference-counting errors on Python objects that lead to use-after-free, +- crashes triggered by the operator's own error paths, by a malformed + `config.toml`, or by shutting the loop down while tasks are in flight. + +### Out of scope by design + +AsyncioDAT runs Python coroutines inside TouchDesigner's interpreter and loads +callback modules from disk — see the `config.toml` `callback_module_path` and +`[main].paths` settings. Treat `.toe` files and any configured callback modules +or `sys.path` entries as **trusted code**: a malicious project file can run +arbitrary Python, exactly as a native TouchDesigner project can. A report whose +premise is that an untrusted `.toe` or callback module executes code is +describing the intended design, not a vulnerability. + +Because the operator trusts its hosting project by design, a report that +depends on the project passing deliberately corrupt state is likely to be +treated as a normal bug rather than a vulnerability. Report it as a regular +issue and we will still fix it. diff --git a/TESTING.md b/TESTING.md index 4eeeb2b..cdb3289 100644 --- a/TESTING.md +++ b/TESTING.md @@ -271,10 +271,4 @@ asyncio_op.add_task(heartbeat()) ## Performance Monitoring -The AsyncioDAT operator provides real-time statistics: -- Execute count (frames processed) -- Event loop status -- Auto-processing state -- Available methods - -Monitor these values to ensure proper operation and identify potential issues. +Attach an Info CHOP to the AsyncioDAT operator for real-time statistics: `event_loop_active`, `event_loop_auto_poll`, `event_loop_poll_count`, and `event_loop_poll_duration` (milliseconds spent in the last poll). Monitor these values to ensure proper operation and identify potential issues — a poll duration that climbs with the number of tasks is the signal that something in a coroutine is blocking rather than awaiting. diff --git a/src/asyncio_dat.cpp b/src/asyncio_dat.cpp index 0392cea..792577f 100644 --- a/src/asyncio_dat.cpp +++ b/src/asyncio_dat.cpp @@ -173,8 +173,10 @@ AsyncioDAT::execute(DAT_Output* output, const OP_Inputs* inputs, void* reserved1 auto active = static_cast(inputs->getParInt("Active")); if (active && !m_asyncioInitialized) { - if (!initializeAsyncio()) { - if (m_status_messages.empty() || strcmp(m_warning, m_status_messages.back().c_str()) != 0) { + // m_warning is null unless initializeAsyncio() took the one path that + // does not log its own failure. + if (!initializeAsyncio() && m_warning) { + if (m_status_messages.empty() || m_status_messages.back() != m_warning) { addStatusMessage(m_warning); } }