Skip to content

Add gomc-rest Python package with bundled server (Pattern B)#1

Merged
Moge800 merged 8 commits into
mainfrom
claude/wizardly-allen-lnrqxf
Jun 27, 2026
Merged

Add gomc-rest Python package with bundled server (Pattern B)#1
Moge800 merged 8 commits into
mainfrom
claude/wizardly-allen-lnrqxf

Conversation

@Moge800

@Moge800 Moge800 commented Jun 27, 2026

Copy link
Copy Markdown
Owner

Summary

Introduces a new Python package that bundles the gomc-rest server binary and auto-launches it as a subprocess, providing a seamless interface for communicating with Mitsubishi PLCs. This implements "Pattern B" — users never need to manually start or distribute the server executable.

Key Changes

  • Core server lifecycle management (src/gomc_rest/_server.py):

    • Server class that spawns the bundled gomc-rest binary on a free loopback port
    • Automatic health checks and version validation on startup
    • Graceful shutdown via context manager or atexit handler
    • Per-launch random bearer token for access control (can be disabled or shared)
    • Optional server_mode to bind all interfaces for network access
  • Public API (src/gomc_rest/__init__.py):

    • launch() function as the primary entry point
    • Re-exports PLCClient and exception types from gomc-rest-client
    • Clean, documented interface for context-manager usage
  • Platform-specific binary resolution (src/gomc_rest/_binaries.py):

    • Detects current platform (Windows/Linux, amd64/arm64)
    • Locates the correct bundled binary
    • Ensures executable permissions on Unix platforms
    • Clear error messages for unsupported platforms or missing binaries
  • Binary vendoring (scripts/vendor_binaries.py):

    • Downloads pinned gomc-rest binaries from GitHub releases
    • Supports per-platform builds (used in CI/CD)
    • Respects GOMC_REST_VERSION file or environment variable
  • Release automation (.github/workflows/release.yml):

    • Builds one platform-specific wheel per OS (Windows amd64, Linux amd64, Linux arm64)
    • Each wheel bundles only its matching binary
    • Publishes to PyPI via trusted publishing (OIDC)
  • Project configuration (pyproject.toml):

    • Declares dependency on gomc-rest-client>=0.10.0
    • Configures wheel building with binary inclusion
    • Metadata for PyPI
  • Documentation (README.md):

    • Usage examples (context manager and standalone)
    • Access control explanation (bearer token + loopback binding)
    • Version compatibility notes
    • Binary vendoring workflow

Notable Implementation Details

  • Security by default: Random per-launch tokens + loopback-only binding prevent unauthorized access even if the port is discovered
  • Graceful degradation: Subprocess termination handles both normal exit and timeout scenarios (SIGTERM → SIGKILL)
  • Version safety: Validates bundled server version against client's minimum supported version at startup
  • Platform-specific wheels: CI/CD produces lean wheels (each ~10–20 MB) instead of shipping all three binaries in every install
  • No git-committed binaries: Binaries are fetched at build time, keeping the repository small

https://claude.ai/code/session_01SQbVkPEprK2ebcjck5ouUy

claude added 8 commits June 25, 2026 09:55
Client-independent groundwork for gomc-rest-python. Adds the package
metadata and the platform binary resolver that locates the bundled
gomc-rest server. The HTTP layer is delegated to gomc-rest-client; the
launch() wiring is intentionally deferred until the in-progress
gomc-rest-client update lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbVkPEprK2ebcjck5ouUy
Add the deferred client-dependent layer now that gomc-rest-client is
updated:
- _server.py: spawn bundled gomc-rest on a free port, wait for /health,
  verify version support, stop via context manager / atexit.
- __init__.py: launch() returning a managed Server; re-export PLCClient
  and exceptions.
- Pin gomc-rest-client>=0.9.0 (requires bundled server v1.3.0+).
- README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbVkPEprK2ebcjck5ouUy
launch()/Server gain server_mode (default False). Default binds the
bundled server to 127.0.0.1 so no other app or host can reach the
unauthenticated REST API; server_mode=True binds all interfaces so other
apps on a trusted network can call it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbVkPEprK2ebcjck5ouUy
gomc-rest and gomc-rest-client now support token auth. launch()/Server
generate a random token per launch, pass it via -token to the bundled
server, and set it on the PLCClient. This makes the default effectively
script-only: another process on the same host that learns the port still
cannot call the API without the token.

- token param: None auto-generates; explicit string to share; "" disables.
- Expose server.token; re-export GomcRestUnauthorizedError.
- Pin gomc-rest-client>=0.10.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbVkPEprK2ebcjck5ouUy
- GOMC_REST_VERSION pins the bundled server (v1.3.0, the client's minimum).
- scripts/vendor_binaries.py downloads the binaries from the matching
  gomc-rest GitHub release (all, or one via --only) into the package.
- release.yml builds one platform-specific wheel per OS (win_amd64,
  manylinux2014 x86_64/aarch64), each bundling only its matching binary,
  and publishes to PyPI via trusted publishing on a v* tag.
- binaries/.gitignore keeps downloaded binaries out of git.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbVkPEprK2ebcjck5ouUy
…oke test

P1: bump GOMC_REST_VERSION to v1.4.0 (v1.3.0 lacks -token and is below the
client's MINIMUM_SUPPORTED_GOMC_REST_VERSION), and add a smoke test that
launches with default settings.

P2:
- token="" now disables auth correctly: distinguish unspecified (None ->
  generate) from "" (disable), and always pass -token explicitly (empty when
  disabled) so an inherited GOMCR_TOKEN can't enable server-side auth while
  the client stays unauthenticated.
- Wrap post-Popen startup validation in try/except and close() the
  subprocess on any failure, since the caller never receives a Server.
- close() now also releases the PLCClient HTTP session and atexit.unregister()s
  itself, so repeated launch/close no longer accumulates connections/objects.
- release.yml verifies the pushed tag matches project.version before building.

Add ci.yml to vendor the Linux binary and run the smoke test on PRs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbVkPEprK2ebcjck5ouUy
… docs

- _binaries.py: chmod only when the executable bit is actually missing, so a
  non-owner running a root-installed, already-executable binary no longer hits
  PermissionError; raise a clear error if a needed chmod is denied. (P1)
- pyproject.toml: cap gomc-rest-client to >=0.10.0,<0.11 so a future client
  raising its minimum server version can't break installs of this package. (P2)
- _server.py: pass the token via GOMCR_TOKEN in a copied env instead of the
  -token argv, so it isn't visible in the process list / /proc cmdline. (P2)
- release.yml: gate the publish job on github.ref_type == 'tag' so
  workflow_dispatch builds but never publishes from an arbitrary branch. (P2)
- README: bundled version v1.4.0, dependency cap, documented threat model
  (same-OS-user can read the env), and a release procedure that bumps both
  project.version and __version__ to match the tag. (P2)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbVkPEprK2ebcjck5ouUy
P1 (glibc tag): the v1.4.0 amd64 binary is dynamically linked and needs
GLIBC_2.34, so tag its wheel manylinux_2_34_x86_64 instead of manylinux2014
(glibc 2.17), which would let pip install it on systems where it fails with
'GLIBC_2.34 not found'. The arm64 binary is statically linked (verified: no
dynamic section), so the wider manylinux2014_aarch64 tag stays correct. Add a
floor-glibc-amd64 CI job that runs the smoke test inside a glibc-2.34 container
so the binary is proven to start at its declared floor.

P2 (supply chain): pin the trusted SHA-256 of each release asset in
checksums/<version>.sha256 (committed, not fetched alongside the binary) and
verify every download in vendor_binaries.py, failing closed on mismatch or a
missing pin. Document adding the checksum file when bumping the bundled server.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbVkPEprK2ebcjck5ouUy
@Moge800 Moge800 merged commit f9ccdf6 into main Jun 27, 2026
2 checks passed
@Moge800 Moge800 deleted the claude/wizardly-allen-lnrqxf branch June 27, 2026 10:42
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.

2 participants