Skip to content

Full SWIG wrap: DCContext managers, EisPyContext, listener directors - #11

Merged
transfix merged 80 commits into
masterfrom
qt6-migration
Jul 3, 2026
Merged

transfix merged 80 commits into
masterfrom
qt6-migration

Conversation

@transfix

@transfix transfix commented Mar 24, 2026 •

Copy link
Copy Markdown
Owner

Full SWIG wrap: DCContext managers, EisPyContext, listener directors

Overview

Complete Python SWIG bindings for libeiskaltdcpp — 4 phases of modular .i file wrapping, class renaming, listener directors, and end-to-end integration testing against a live NMDC hub.

Phase 1: Modular SWIG manager wrapping (1f28d58)

  • 17 modular .i files wrapping all dcpp managers via DCContext typed accessors
  • dcpp_types.i for CID, TTHValue, UserPtr, HintedUser, etc.
  • dcpp_context.i with typed manager properties (settings_manager, queue_manager, etc.)
  • eispy_context.i with %extend shortcuts for EisPyContext
  • 108 tests in test_swig_managers.py

Phase 2: SettingsManager + FavoriteManager enums (af200af)

  • Full enum wrapping for all SettingsManager int/string/bool/int64 settings
  • FavoriteManager SWIG wrapping with HubEntry class
  • 503 tests covering all settings enums and manager access

Phase 3: Remove DCBridge → EisPyContext (f54d288)

  • Renamed DCBridge → EisPyContext across all C++/SWIG/Python code
  • Removed bridge.h / bridge.cpp (replaced by eispy_context.h/.cpp)
  • DCBridge = EisPyContext Python alias for backward compatibility
  • Migrated DCClient.py to use direct manager calls
  • 181 tests passing (50 dc_core + 108 swig_managers + 23 others)

Phase 4: Full listener directors (2cf8343)

  • 7 Python-subclassable listener adapter classes in listener_adapters.h:
    • PyClientListener — 15 per-hub events (connect, chat, users)
    • PyClientManagerListener — 7 global user/hub lifecycle events (NEW)
    • PySearchManagerListener — search result events
    • PyQueueManagerListener — all 18 queue events
    • PyDownloadManagerListener — 5 download transfer events
    • PyUploadManagerListener — 6 upload transfer events
    • PyTimerManagerListener — second/minute tick events
  • Adapters convert dcpp tag-dispatched on() to named virtual methods
  • Pass Python-safe types (strings, ints, types.h structs) — no raw C++ pointers
  • SWIG directors + threads="1" handle GIL automatically
  • 14 subscribe/unsubscribe methods on EisPyContext
  • BridgeListeners default multiplexer unchanged
  • 89 new tests in test_swig_listeners.py

Phase 5: Integration testing & ABI fix

End-to-end integration tests against a live NMDC hub (nmdcs://wintermute.sublevels.net:411), exercising the full stack from Python through SWIG directors into the C++ core and back.

Integration test suite (test_integration.py, test_file_transfer.py)

  • 16 integration tests covering hub connect/disconnect, user enumeration, chat, search, and peer-to-peer file transfer
  • Two independent EisPyContext instances (alice + bob) connecting to a real hub simultaneously
  • File transfer test: bob (passive, INCOMING_FIREWALL_PASSIVE) requests alice's (active) file list via $RevConnectToMe → $ConnectToMe → TLS peer connection
  • Custom DCClientCallback subclass with async event capture via asyncio.Event

ABI mismatch fix: LUA_SCRIPT propagation (5bf4d28)

  • Root cause: dcpp::Client conditionally inherits ClientScriptInstance when LUA_SCRIPT is defined, adding an 8-byte vtable pointer that shifts all member offsets. The bridge library's target_compile_definitions used PRIVATE visibility, so the SWIG dc_core module compiled Client.h without the define — ABI mismatch between the dcpp shared library and the SWIG wrapper.
  • Symptom: hubUrl.size() returned 0x55A4E7E34DF0 (~85 TB) because the _M_string_length field was read at offset−8, landing on the _M_dataplus._M_p (heap pointer) field instead. The subsequent string copy attempted to allocate ~85 TB → std::bad_alloc.
  • Fix: Changed PRIVATE → PUBLIC for target_compile_definitions(eiskaltdcpp_py_bridge ... LUA_SCRIPT) and both target_include_directories calls in the if(EISKALTDCPP_HAS_LUA) block. CMake's transitive property system now propagates -DLUA_SCRIPT and the Lua/stub include paths to all consumers of the bridge library, including the SWIG module.

Upstream eiskaltdcpp fixes (pinned at c9940e1e)

  • Comprehensive noexcept removal — 414 incorrect noexcept specifiers removed across 61 dcpp files; exceptions now propagate instead of calling std::terminate
  • Speaker::fire() exception safety — try/catch around listener dispatch
  • Thread stack reduction — pthread_create with 1 MiB stacks on POSIX (down from 8 MiB default), reducing virtual memory from ~500 MB to ~100 MB on busy hubs
  • ConnectionManager leak fix — nmdcConnect()/adcConnect() catch all exceptions from connect(), properly cleaning up UserConnection objects
  • noexcept removal in bridge — all listener on() overrides in bridge_listeners.h and listener_adapters.h had noexcept removed to match upstream

Test coverage

  • 732 tests across 16 test files
  • 924 pytest tests passing in CI (including parameterized variants)
  • 16 integration tests against live NMDC hub
  • All 8 CI build jobs green (Ubuntu, self-hosted Linux, Windows MSVC, Windows MinGW, macOS)
  • All adapter classes instantiable from Python
  • Python subclassing with method override verified
  • Director identity and multiple instance tests

CI/CD pipeline

  • 8 parallel build jobs: Ubuntu 24.04, self-hosted Linux x64, Windows MSVC, Windows MinGW (UCRT64), macOS 14 (ARM64), macOS 13 (x86_64), plus dedicated pytest and integration test jobs
  • Integration tests run on self-hosted runner with real network access to NMDC hub
  • FetchContent pins eiskaltdcpp at a specific commit SHA for reproducible builds
  • Cross-platform: Linux, Windows (MSVC + MinGW), macOS (ARM + x86_64)

Usage example

import dc_core

class MyQueueListener(dc_core.PyQueueManagerListener):
    def onAdded(self, target, size, tth):
        print(f"Queued: {target} ({size} bytes)")
    def onFinished(self, target, size, dir):
        print(f"Done: {target}")

ctx = dc_core.EisPyContext()
ctx.initialize('/tmp/dc-config')
listener = MyQueueListener()
ctx.addQueueListener(listener)

transfix added 14 commits March 23, 2026 21:14
- Point FetchContent to github.com/transfix/eiskaltdcpp qt6-migration branch
- Bump C++ standard from 17 to 20 (required by qt6-migration)
- Add USE_QT6=OFF to disable Qt6 GUI (we only need the core library)
- Patch CMAKE_SOURCE_DIR/CMAKE_BINARY_DIR -> CMAKE_CURRENT_* for
  FetchContent compatibility (upstream uses top-level project paths)
- Disable eiskaltdcpp's own BUILD_TESTS to avoid leaking from parent
- Migrate from Singleton::getInstance() to DCContext accessor pattern:
  Manager::getInstance() -> getContext()->getManager()
  (dcpp managers no longer inherit from Singleton<T>)
- Use FetchContent_Populate + manual sed patch + add_subdirectory
  instead of FetchContent_MakeAvailable for patch control
- Use -DUSE_SYSTEM_EISKALTDCPP=OFF so CI builds against the
  qt6-migration branch via FetchContent instead of the old
  system-installed libeiskaltdcpp (which lacks the new
  DCContext/getContext() API)
- Add ubuntu-24.04 GitHub-hosted runner alongside the self-hosted
  runner in the build matrix
- Replace libeiskaltdcpp-dev with the actual build deps needed by
  FetchContent (miniupnpc, idn2, pcre2, lua)
- Self-hosted runner failures are non-blocking (continue-on-error)
- Add build-and-test-windows job (MSVC, vcpkg, Ninja, SWIG via choco)
- Add build-and-test-macos job (brew deps, openssl@3)
- Add Windows and macOS wheel builds via cibuildwheel (delvewheel, delocate)
- Add macOS ARM64 + x86_64 wheel matrix entries
- Fix CMakeLists.txt: replace sed -i with portable file(READ)/string(REPLACE)
- Fix swig/CMakeLists.txt: portable .so/.pyd suffix, Windows copy vs symlinks
- Update pyproject.toml OS classifiers for Windows and macOS
- Switch sdist/publish jobs to GitHub-hosted ubuntu-24.04 runners
- FreeBSD via vmactions/freebsd-vm (pkg)
- OpenBSD via vmactions/openbsd-vm (pkg_add)
- NetBSD via vmactions/netbsd-vm (pkg_add)
- DragonflyBSD via vmactions/dragonflybsd-vm (pkg)
- OmniOS/illumos via vmactions/omnios-vm (pkg) for Solaris compatibility
- All BSD/Solaris jobs are continue-on-error (experimental)
- Add BSD and Solaris OS classifiers to pyproject.toml
- Haiku: cross-compile using haiku/cross-compiler:x86_64-r1beta5 Docker image
- GNU/Hurd: cross-compile using i686-linux-gnu toolchain as approximation
- Both are continue-on-error, build-only (no tests), warnings on failure
- Added to integration job dependency chain
- Haiku: use x86_64-r1beta4 Docker image (r1beta5 doesn't exist)
- Hurd: embed Python3 paths in toolchain file, use BOTH for find modes
- FreeBSD: pin release 14.2, explicit python311/py311-pip, libidn2-0, ninja-build
- OpenBSD: pin release 7.6, explicit python-3.11.10, lua-5.4.7 package names
- NetBSD: pin release 10.1, drop /usr/sbin prefix, remove --break-system-packages
- DragonflyBSD: pin release 6.4.0, explicit python311 package
- OmniOS: bump timeout to 45 minutes
- macOS: add CMAKE_PREFIX_PATH for all brew-installed deps
eiskaltdcpp's custom FindGettext.cmake requires xgettext, msgfmt, etc.
All platform builds were failing because these tools were missing.

- Ubuntu: add 'gettext' package (both build-and-test and integration jobs)
- Windows: add 'gettext' to vcpkg install
- macOS: add 'gettext' to brew install + CMAKE_PREFIX_PATH
- FreeBSD/DragonflyBSD: add 'gettext-tools' to pkg install
- OpenBSD/NetBSD: add 'gettext-tools' to pkg_add
- OmniOS: add 'text/gnu-gettext'
- Haiku/Hurd cross-compile: add 'gettext' to apt-get install
- Wheels: add 'gettext-devel' to manylinux dnf install
Apple Clang lacks std::jthread which eiskaltdcpp's Thread.h requires.
Switch to Homebrew GCC (auto-detected version) for both CI and wheels.
Also add gettext and gcc to macOS brew install, update CMAKE_PREFIX_PATH.
- macOS: export CPATH/LIBRARY_PATH so Homebrew GCC finds brew headers/libs
  (GCC doesn't search /opt/homebrew by default unlike Apple Clang)
- Haiku: add 'python3' package alongside python3-dev in container
- Hurd: add 'python3' package alongside python3-dev
- Wheels: add CPATH/LIBRARY_PATH to CIBW_ENVIRONMENT_MACOS
FreeBSD: libidn2-0 → libidn2, ninja-build → ninja, drop unneeded gcc
OpenBSD: python-3.11.10 → python%3.11, lua-5.4.7 → lua%5.4, drop bzip2 (in base)
NetBSD: use /usr/sbin/pkg_add, drop miniupnpc, add gcc-13 compiler flags
DragonflyBSD: add gcc13 for C++20, add git
OmniOS: developer/build/cmake → ooce/developer/cmake, drop runtime/lua (N/A),
  add ooce/developer/ninja + git, use gcc14 from /opt/gcc-14/bin

Remove Haiku and Hurd cross-compilation jobs — will add native runners later.
Remove FreeBSD, OpenBSD, NetBSD, DragonflyBSD, and OmniOS CI jobs —
the vmactions VMs are too fragile for now; will revisit with native
runners later.

Fix Windows gettext: install CLI tools (xgettext, msgfmt, etc.) from
MSYS2 pacman since vcpkg only provides libintl for linking. Add
C:\msys64\usr\bin to PATH so FindGettext.cmake can find them.
- Remove redundant actions/cache on C:/vcpkg/installed — the native
  VCPKG_BINARY_SOURCES=x-gha caching is more efficient per-package.
- wheels.yml: add MSYS2 gettext tools install in CIBW_BEFORE_ALL_WINDOWS,
  add C:\msys64\usr\bin to PATH and VCPKG_BINARY_SOURCES to
  CIBW_ENVIRONMENT_WINDOWS for proper caching inside cibuildwheel.
…acts

- bridge.cpp: wrap dlfcn.h/unistd.h in #ifndef _WIN32; use
  <process.h>/_getpid() and <windows.h> on Windows. Skip
  dlsym-based Lua runtime resolution on Windows (MSVC uses
  different name mangling; Itanium ABI symbols not available).

- ci.yml: upload build artifacts with naming convention
  eiskaltdcpp-py-<version>~dev-<sha>-<os>-<arch>-py<pyver>
  so PR artifacts are clearly marked as unreleased.

- wheels.yml: rename wheel/sdist artifacts to match naming
  convention. Tag builds omit '~dev'; non-tag builds include it.
…ault description

- Update version from 2.4.2 to 2.5.0.0 in CMakeLists.txt, pyproject.toml,
  __init__.py, and bridge.cpp
- Default nick changed from 'dcpy-<pid>' to 'eispy-<pid>'
- Add default description '<username>@<hostname>' for new connections
- Add eispy NMDC/ADC client tags
- Update test assertions for new nick prefix
BridgeListeners was the last Meyer's singleton in either codebase.
Now constructed with DCBridge& reference and owned as a unique_ptr
member of DCBridge, consistent with the singleton-free architecture
in eiskaltdcpp.

Changes:
- bridge_listeners.h: remove static getInstance(), add explicit
  constructor taking DCBridge&, store reference instead of pointer,
  remove setBridge(), simplify attach() signature
- bridge.h: add forward decl + unique_ptr<BridgeListeners> member
- bridge.cpp: replace all 7 BridgeListeners::getInstance() call
  sites with m_listeners-> access
- bridge_listeners.cpp: replace m_bridge-> with bridge_., remove
  null-pointer guard checks (reference always valid)
On Windows with LUA_SCRIPT defined, resolveLuaStatePtr() (which uses
dlsym/RTLD_DEFAULT) is unavailable -- only defined under
"#if defined(LUA_SCRIPT) && not Win32". Four call sites used bare
"#ifdef LUA_SCRIPT", causing C3861 identifier-not-found on MSVC.

Change all four guards to match the function definition.
- dcpp::startup() now returns std::unique_ptr<DCContext>
- DCBridge stores m_context member (owns the dcpp core context)
- shutdown() calls m_context->shutdown() + reset + setContext(nullptr)
- Forward-declare dcpp::DCContext in bridge.h
- Add #include <dcpp/DCContext.h> to bridge.cpp
- All tests pass
- Replace removed SETTING() macro with direct
  m_context->getSettingsManager()->get() calls (DOWNLOAD_DIRECTORY,
  TOTAL_DOWNLOAD, TOTAL_UPLOAD)
- Pass DCContext& to DirectoryListing constructor and
  getUserFromFilename() static (now requires context)
- Add listener detach loop at start of shutdown() to prevent
  segfault from socket-thread callbacks firing on stale Client
  pointers during disconnect
- Add FULL_SWIG_WRAP_PLAN.md to .gitignore
Phase 1 of the full SWIG wrap plan — 20 new modular .i interface files
that expose all dcpp manager classes directly to Python via SWIG.

New files:
- dcpp_types.i: CID, TTHValue, User/UserPtr, HintedUser, Identity,
  HubEntry, FavoriteHubEntry, UserCommand, STL container templates
- dcpp_context.i: DCContext, ContextAware, startup/shutdown helpers,
  forward-declared TimerManager/ResourceManager
- dcpp_settings.i: SettingsManager with all setting enums
- dcpp_client_manager.i: Hub/user operations, messaging, search
- dcpp_search_manager.i: Search modes and operations
- dcpp_queue_manager.i: Queue CRUD, priority, Python context manager
- dcpp_share_manager.i: Share directory management and stats
- dcpp_hash_manager.i: Hashing control and TTH operations
- dcpp_download_manager.i: Download count and speed
- dcpp_upload_manager.i: Upload stats, slots, waiting users
- dcpp_throttle_manager.i: Bandwidth limit controls
- dcpp_favorite_manager.i: Favorites CRUD (hubs/users/dirs/commands)
- dcpp_log_manager.i: Logging areas, messages, paths
- dcpp_finished_manager.i: Finished transfer management
- dcpp_crypto_manager.i: TLS and certificate operations
- dcpp_connectivity.i: ConnectivityManager + MappingManager
- dcpp_ipfilter.i: IP filtering rules (global namespace)
- dcpp_adl_search.i: ADL search patterns and manager
- dcpp_debug_manager.i: Debug message directions
- eispy_context.i: DCBridge Python property accessors for all managers

Modified:
- dc_core.i: Added stdinc.h preamble and %include directives for all
  modular .i files

Key patterns:
- Cherry-pick public methods (no %include of full headers)
- %nodefaultctor on all ContextAware subclasses (DCContext& ctor)
- %extend for __str__/__repr__/__hash__/__eq__ and Python properties
- %nodefaultdtor on DCContext (prevent accidental deletion)

All 529 existing tests pass.
Phase 2 of the full SWIG wrap plan — Python Client Layer + REST API.

DCClient/AsyncDCClient manager properties:
- 16 new @Property accessors: settings, clients, queue, shares,
  search_manager, downloads, uploads, favorites, finished, hashing,
  throttle, connectivity, crypto, logs, ip_filter, adl_search
- Delegate through DCBridge → DCContext → C++ manager instances

SWIG include order fix:
- Split dcpp_context.i into dcpp_context_base.i (ContextAware mixin,
  included before managers) and dcpp_context.i (DCContext class with
  typed manager accessors, included after all manager definitions)
- Fixes SwigPyObject return type for getSettingsManager() etc.
- Added IPFilter accessor via %extend on DCContext

REST API expansion (8 new route modules):
- favorites.py: GET/POST/DELETE hubs + dirs
- throttle.py: GET/PUT bandwidth limits (MAX_UPLOAD/DOWNLOAD_SPEED_MAIN)
- connectivity.py: GET status, POST detect/setup
- crypto.py: GET status, POST certificate generate/reload
- logs.py: GET path by area
- finished.py: DELETE downloads/uploads
- ipfilter.py: POST check IP
- adl.py: GET/POST/DELETE searches

Pydantic models for all new endpoints (~200 lines).
Routes registered in routes/__init__.py.

Tests:
- test_swig_managers.py: 108 tests for SWIG types, manager properties
- test_rest_phase2.py: 23 tests for all new REST endpoints
- All 660 tests pass (131 new + 529 existing)
Phase 3 of the full SWIG wrap plan — Remove DCBridge.

EisPyContext replaces DCBridge as the main Python-facing class:
- New src/eispy_context.h/cpp with trimmed API (~830 LOC vs 1445)
- Deleted src/bridge.h/cpp
- 'using DCBridge = EisPyContext;' C++ alias for backward compat
- '%pythoncode { DCBridge = EisPyContext }' Python alias in SWIG

15 pass-through methods removed (now direct SWIG manager access):
- Queue: removeFromQueue, moveQueueItem, setPriority
- Sharing: removeShareDir, renameShareDir, listShare, refreshShare,
  getShareSize, getSharedFileCount
- Transfers: getTransferStats
- Hashing: getHashStatus, pauseHashing
- Settings: getSetting, setSetting, reloadConfig

DCClient migration:
- Constructor uses dc_core.EisPyContext()
- Removed methods delegate to self.queue, self.shares, self.hashing,
  self.settings (direct SWIG manager properties from Phase 2)
- return-bool wrappers added for share ops (try/except around void)

Updated files:
- bridge_listeners.h/cpp: DCBridge& → EisPyContext&
- src/CMakeLists.txt: source file references
- swig/dc_core.i: class rename, docstrings, removed properties
- swig/eispy_context.i: updated includes and class references
- tests: EisPyContext usage, alias verification, removed-method checks
@transfix transfix changed the title Switch to eiskaltdcpp qt6-migration branch Full SWIG wrap: DCContext managers, EisPyContext, listener directors Mar 30, 2026
Create Python-subclassable listener adapter classes for all 7 dcpp
listener interfaces. Each adapter inherits from the dcpp listener,
overrides tag-dispatched on() methods, and dispatches to named virtual
methods (onConnected, onFailed, onAdded, etc.) that SWIG directors
expose to Python.

Adapter classes (src/listener_adapters.h):
  - PyClientListener        — 15 hub events (connect, chat, users)
  - PyClientManagerListener — 7 events (NEW, not in BridgeListeners)
  - PySearchManagerListener — 1 event (search results)
  - PyQueueManagerListener  — 18 events (all dcpp queue events)
  - PyDownloadManagerListener — 5 events (transfer progress)
  - PyUploadManagerListener — 6 events (transfer + waiting)
  - PyTimerManagerListener  — 2 events (second + minute tick)

All adapters pass Python-safe types (strings, ints, types.h structs)
rather than raw dcpp pointers. SWIG directors + threads=1 handle GIL.

EisPyContext gains 14 subscribe/unsubscribe methods:
  addHubListener/removeHubListener (per-hub ClientListener)
  addClientManagerListener/removeClientManagerListener (NEW)
  addSearchListener/removeSearchListener
  addQueueListener/removeQueueListener
  addDownloadListener/removeDownloadListener
  addUploadListener/removeUploadListener
  addTimerListener/removeTimerListener

BridgeListeners (default convenience multiplexer) is unchanged.

89 new tests in test_swig_listeners.py — all pass.
Root cause 1 — DCContext SWIG heap corruption:
  SWIG's view of DCContext omitted the conditional #ifdef WITH_DHT
  and #ifdef LUA_SCRIPT members (two extra unique_ptrs = 16 bytes).
  Python-side 'new DCContext()' allocated an undersized object;
  the C++ constructor then zeroed past the allocation, silently
  corrupting heap metadata.  Crash manifested later as
  'corrupted size vs. prev_size' / SIGSEGV during unrelated allocs.

  Fix: %nodefaultctor on DCContext — Python can no longer construct
  it directly.  All access goes through getContext() / EisPyContext.

Root cause 2 — EisPyContext sequential init/shutdown:
  dcpp's DCContext::shutdown() + Util::uninitialize() leave the
  library in a non-restartable state.  Calling dcpp::startup() a
  second time corrupted memory.

  Fix: make dcpp a permanent process-lifetime singleton.
  EisPyContext::shutdown() cleans only per-instance state (hubs,
  listeners, file lists).  The DCContext is never destroyed — the
  OS reclaims resources at exit.

Tests: 284 non-server tests pass (exit 0), no heap corruption.
SWIG wrappers:
- dcpp_connection_manager.i: port accessors, connect/disconnect/shutdown
- dcpp_dyndns.i: load/stop
- dcpp_context.i: add getConnectionManager(), getDynDNS() accessors
- eispy_context.i: connection_manager, dyndns properties
- dc_client.py, async_client.py: connection_manager, dyndns properties
- dc_core.i: include new .i files

Tests:
- conftest.py: session-scoped bcrypt.gensalt patch (rounds=4) — fixes
  apparent test hang caused by ~0.35s per hash × 91 tests = 91s runtime
- test_swig_managers.py: ConnectionManager + DynDNS type/property tests

757 passed, 21 deselected in ~49s (was ~91s before bcrypt fix)
- Guard TestTLSEncryption with TEST_HUB_URL env var (skip when unset)
  instead of defaulting to a live hub that crashes on macOS CI
- Fix async_client fixture: use object.__new__ to avoid creating an
  orphaned DCClient whose EisPyContext C++ destructor can run during
  GC while the real instance is active
…t async

- Add DCClientProtocol (runtime_checkable Protocol) in protocol.py
  defining the unified async interface for both local and remote clients
- Make all AsyncDCClient query/mutation methods async to match protocol
- Add missing protocol methods: set_priority, reload_config,
  get_transfer_stats, get_hash_status, get_share_size, get_shared_files
- Remove all _async backward-compat aliases from RemoteDCClient
- Remove TestSyncMethodsRaiseTypeError (obsolete — methods are now coroutines)
- Update all callers: tests, examples, README
- Export DCClientProtocol from eiskaltdcpp.__init__
- Update README: architecture diagram, project structure, wheel platforms,
  RemoteDCClient examples, new DCClientProtocol section
- Add RUNTIME_OUTPUT_DIRECTORY so .pyd (DLL) goes to correct location
  on Windows (LIBRARY_OUTPUT_DIRECTORY only applies to .lib import libs)
- Add per-config directory overrides (Release/Debug) to suppress
  multi-config generator subdirectories
- Add protocol.py to POST_BUILD copy list (new file from API unification)
Add hub alias system so users can type 'eispy hub connect winter'
instead of 'eispy hub connect nmdcs://wintermute.sublevels.net:411'.

New module: python/eiskaltdcpp/hub_aliases.py
  - load/save aliases from ~/.config/eispy/hubs.json
  - EISPY_HUBS_FILE env var override, XDG_CONFIG_HOME support
  - resolve() transparently passes through URLs (contains ://)
    or looks up aliases with clear error messages

New CLI commands: eispy hub alias add/rm/ls
  - Validates URL scheme on add (dchub://, nmdcs://, adc://, adcs://)

Alias resolution integrated into all hub-accepting CLI commands:
  - hub connect/disconnect/users (positional args)
  - chat send/pm/history (positional args)
  - search query/results/clear --hub (options)
  - queue add --hub (option)
  - filelist request (positional arg)
  - daemon/up --hub (multiple option)

All four DC URL schemes are supported: dchub://, nmdcs:// (TLS),
adc://, adcs:// (ADC+TLS).

37 new tests in tests/test_hub_aliases.py covering:
  - Core module: load/save, add/remove, resolve, config paths
  - CLI: alias add/rm/ls subcommands
  - Resolution: connect, disconnect, chat, search, filelist with aliases
transfix added 29 commits April 6, 2026 01:40
… listeners)

This commit removes noexcept from all 320 listener on() methods and
ConnectionManager allocation functions, allowing Speaker::fire()'s
try/catch to catch std::bad_alloc instead of triggering std::terminate().
…pt removal)

This commit removes all remaining noexcept from dcpp library functions,
including BufferedSocket::write() which was the most likely source of the
persistent std::bad_alloc -> std::terminate crash during file list serving.
The dcpp base Listener classes no longer have noexcept on their
virtual on() methods. Keeping noexcept on the overrides in
bridge_listeners.h and listener_adapters.h would cause std::terminate
if any exception propagates from within the callback.
…FT test

Limits $GetINFO requests to 10 per $NickList to prevent memory
exhaustion from $MyINFO flood on large hubs.
…ostics

Pin eiskaltdcpp to 72a39287 (file transfer tracing in QM::addList,
CM::onSecond, ClientManager::connect, NmdcHub::connectToMe).
Also add fprintf tracing to EisPyContext::requestFileList.
The bad_alloc in nmdcConnect was heap corruption, not memory exhaustion.
Capping user creation was wrong and didn't fix it.  Instead:

- Bob now uses passive mode (INCOMING_CONNECTIONS=2).  The flow becomes:
    bob → hub: $RevConnectToMe bob alice
    alice → hub: $ConnectToMe bob <alice_ip>:<port>  (just sends a string, no alloc)
    bob processes incoming $ConnectToMe on his clean heap
- Alice stays active with TCP listener on port 4200/4201
- Revert user creation caps in eiskaltdcpp (all getUser paths uncapped)
- NMDC_GETINFO_LIMIT now only limits $GetINFO requests (original purpose)
- Increase NMDC_GETINFO_LIMIT from 10 to 100 for the test
dcpp::Client conditionally inherits ClientScriptInstance when
LUA_SCRIPT is defined, adding an 8-byte vtable pointer that shifts
all member offsets.  The bridge had PRIVATE visibility for the
-DLUA_SCRIPT define, so the SWIG dc_core module compiled Client.h
without it — ABI mismatch.

Symptom: hubUrl.size() returned 0x55A4E7E34DF0 (a heap pointer)
because the _M_string_length field was read at offset-8, landing
on the _M_dataplus._M_p field instead.  The string copy then
attempted to allocate ~85 TB → std::bad_alloc.
The 'Singleton pattern prevents safe restart' blocker no longer applies:
transfix/eiskaltdcpp removed the Singleton<> CRTP base entirely; a
DCContext object owns all managers via dependency injection, so multiple
independent contexts coexist and startup()/shutdown() cycles are bounded
by context lifetime. Rewrite the TODO section accordingly.
Repin the FetchContent GIT_TAG from the old development commit
c9940e1e to the v2.5.0 release tag (full SHA
3f01cb083a2b2d83c5944dd69d451df8401cc692) so the bindings build
against the first tagged transfix/eiskaltdcpp release.

Verified locally: a fresh isolated wheel build against this pin
(USE_SYSTEM_EISKALTDCPP=OFF) succeeds and produces
eiskaltdcpp_py-2.5.0.0.
Prefer a prebuilt, toolchain-matched core SDK over recompiling the core
from source in every wheel, with a graceful fallback to the existing
FetchContent source build when no SDK archive is present.

- CMakeLists.txt: new EISKALTDCPP_SDK_DIR cache path, searched first
  (NO_DEFAULT_PATH) so a stale system copy cannot shadow the pinned
  version. Three tiers: SDK dir -> system package -> FetchContent. The
  imported target handles Windows import-lib + DLL, and the Lua-ABI
  autodetect is shared across the SDK and system paths.
- .github/workflows/wheels.yml: EISKALTDCPP_CORE_TAG=v2.5.0; Linux and
  macOS CIBW_BEFORE_ALL best-effort download+extract the matching
  eiskaltdcpp-core-<tag>-<plat> archive to $HOME/.eiskaltdcpp-sdk/core
  and pass -DEISKALTDCPP_SDK_DIR to CMake. If the download fails, the
  source build path is used unchanged. Windows keeps the source build
  (already MSVC/vcpkg-matched).
…::Module

Fixes the first end-to-end wheels.yml run (SDK-consume path):

- CMakeLists.txt: request Python3 COMPONENTS ... Development.Module instead
  of the full Development component. Development pulls in Development.Embed,
  which needs a linkable libpython that manylinux images do not ship, so the
  Linux wheel config failed with "Could NOT find Python3 (missing:
  Python3_LIBRARIES Development)".

- swig/CMakeLists.txt: link the SWIG extension against Python3::Module rather
  than Python3::Python (Embed). Extension modules must not link libpython on
  Unix (symbols resolve at load time); Windows still links pythonXX.lib via
  the Module target.

- wheels.yml (macOS): cibuildwheel's bashlex evaluator for CIBW_ENVIRONMENT
  does not support pipelines inside $(...). Resolve the highest-versioned
  Homebrew GCC in CIBW_BEFORE_ALL_MACOS (real bash) and expose it via stable
  symlinks under $HOME/.gccwrap, referenced without a pipe in CMAKE_ARGS.

- wheels.yml: add bcrypt to CIBW_TEST_REQUIRES (tests/conftest.py imports it
  at module level; Windows built the wheel with the SDK but failed at test on
  ModuleNotFoundError: No module named 'bcrypt').
The extension now builds and links against the prebuilt SDK on all
platforms, but the repair step failed:

- Linux: auditwheel "Cannot repair wheel, because required library
  libeiskaltdcpp.so.2.5 could not be located".
- macOS: delocate "Could not find all dependencies".

The SWIG extension records the core lib as a RUNPATH (Linux) / @rpath
(macOS) dependency, but had no rpath pointing at the SDK lib directory,
so the repair tools could not find the shared library to bundle it.

- CMakeLists.txt: derive EISKALTDCPP_LIB_DIR from the resolved library.
- swig/CMakeLists.txt: append EISKALTDCPP_LIB_DIR to the extension's
  BUILD_RPATH and INSTALL_RPATH (non-Windows) so auditwheel/delocate can
  locate and vendor libeiskaltdcpp.so.2.5 / libeiskaltdcpp.2.5.dylib.
- wheels.yml: also export LD_LIBRARY_PATH to the SDK lib dir on Linux as
  belt-and-suspenders for auditwheel.
- Remove the macos-13 (x86_64) wheel matrix entry — GitHub has retired the
  Intel macOS runners.
- Replace the ad-hoc CIBW_TEST_REQUIRES list with CIBW_TEST_EXTRAS
  "api,test,api-test" so the wheel's declared extras (fastapi, httpx,
  bcrypt, pydantic, pytest, ...) are installed for the test suite. Fixes
  ModuleNotFoundError for fastapi/httpx after the RPATH repair fix made the
  build+repair stages pass.
Build + SDK link + wheel-repair now all pass; only the test step remained:

- macOS: the prebuilt core dylib targets macOS 13.0
  (CMAKE_OSX_DEPLOYMENT_TARGET=13.0), but the wheel was tagged macosx_11_0,
  so delocate rejected the vendored dylib ("Library dependencies do not
  satisfy target MacOS version 11.0"). Set MACOSX_DEPLOYMENT_TARGET=13.0 so
  the wheel platform tag matches.

- Linux (900 passed, 24 failed): tests/ has no __init__.py and some tests do
  `from tests.test_websocket import ...`. Run pytest via `cd {project} &&
  python -m pytest tests/` so the project root lands on sys.path (implicit
  namespace package), resolving the `No module named 'tests'` errors.
delocate failed: the bundled Homebrew gcc-16 libstdc++.6.dylib on the
macos-latest runner (now macOS 26) requires macOS 26.0, so the wheel could
not be tagged for an older OS. Pin the macOS runner to macos-14 — the same
image the prebuilt core SDK arm64 is built on — so the Homebrew runtime
deps target macOS 14, and set MACOSX_DEPLOYMENT_TARGET=14.0 inside
CIBW_ENVIRONMENT_MACOS (the step-level env does not propagate into
cibuildwheel's isolated build) so the wheel tag matches its vendored libs.
…ests

The `cd {project} && pytest` approach fixed Linux but failed on Windows:
cibuildwheel's temp cwd is on C: while the checkout is on D:, and `cd D:\...`
in cmd does not switch drives, so pytest reported "file or directory not
found: tests/".

Set pytest `pythonpath = ["."]` in pyproject.toml so the project root
(rootdir) is added to sys.path regardless of cwd, making
`from tests.test_websocket import ...` resolve everywhere. Revert
CIBW_TEST_COMMAND to invoke pytest with the absolute {project}/tests path.
The SWIG extension is a SHARED library, so on Windows its .pyd is a RUNTIME
artifact (and the import .lib is ARCHIVE); the install(TARGETS) rule only
declared LIBRARY DESTINATION, which matches neither on Windows. As a result
_dc_core.pyd was silently omitted from the Windows wheel, producing
"ImportError: cannot import name '_dc_core'" at test time (285 native tests
skipped, 2 unguarded ones failed).

Add RUNTIME DESTINATION alongside LIBRARY so the extension is packaged on
Windows while Unix (LIBRARY) is unchanged.
…nix .so)

Adding RUNTIME DESTINATION unconditionally to install(TARGETS dc_core)
regressed BOTH Unix wheels: the .so vanished from the archive (auditwheel:
"no ELF executable or shared library file found"; delocate: "Failed to find
any binary with the required architecture"). The Unix .so is installed via
the LIBRARY artifact (it is gitignored, so wheel.packages does not pick it
up), and the extra RUNTIME kind somehow suppressed it.

Split the rule per platform: Unix keeps the original LIBRARY-only install
(known-good), Windows uses RUNTIME so _dc_core.pyd lands in the wheel.
@transfix
transfix merged commit 5fa455b into master Jul 3, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant