Add producer-friendly FriendNet bridge status and startup - #2
Conversation
This wires the bridge to poll FriendNet, surfaces readable connection state in the UI, and builds the helper binaries needed to make local tester installs behave like one app. Made-with: Cursor
There was a problem hiding this comment.
Pull request overview
This PR upgrades the bridge/UI to report a structured, producer-friendly FriendNet/bridge status, adds FriendNet polling + bundled server autostart in the Go bridge, and updates build/packaging to stage the helper binaries.
Changes:
- Replaced raw
/v1/statusdump in the Vue UI with structured bridge + FriendNet status presentation. - Implemented
/v1/statusengine inwavgang-bridgeto poll FriendNet over Connect RPC and optionally autostart a bundledfriendnet-server. - Updated CI and build tooling to build/package the Go helper binaries and integrate helper staging into the CMake build.
Reviewed changes
Copilot reviewed 12 out of 14 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/src/stores/bridge.ts | Switches the store from raw text status to typed JSON + computed UI strings. |
| ui/src/App.vue | Updates UI to show friendly connection status and retry UX, adds styling. |
| source/HostLauncher.h | Adds FriendNet executable lookup API (for bundled layout parity). |
| source/HostLauncher.cpp | Adds FriendNet executable discovery; changes bridge startup reachability retry behavior. |
| scripts/build-go-binaries.sh | Adds a local script to build bridge + FriendNet server (and admin UI embed). |
| docs/BRIDGE_API.md | Documents expanded /v1/status response and bridge/FriendNet configuration. |
| CMakeLists.txt | Integrates helper staging via BridgeHelpers and wvg_enable_local_helpers. |
| bridge/status.go | Introduces structured status snapshot + FriendNet poller via Connect RPC. |
| bridge/main.go | Wires status engine, signal context, FriendNet autostart, and new /v1/status handler. |
| bridge/launcher.go | Adds FriendNet server discovery, layout bootstrap, and autostart logic. |
| bridge/go.mod / bridge/go.sum | Adds Connect RPC + FriendNet protocol dependencies (with local replace). |
| .gitignore | Ignores staged friendnet-server binaries in bridge/. |
| .github/workflows/build_and_test.yml | Builds friendnet admin UI + Go binaries in CI and uploads artifacts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| include(PamplejuceMacOS) | ||
| include(JUCEDefaults) | ||
| include(Sanitizers) | ||
| include(BridgeHelpers) |
There was a problem hiding this comment.
include(BridgeHelpers) / wvg_enable_local_helpers() are introduced, but there is no BridgeHelpers.cmake (or wvg_enable_local_helpers definition) in the repo checkout outside the cmake submodule. Ensure the cmake submodule pointer is updated in this PR to a revision that provides these modules/functions; otherwise CMake configure will fail for consumers who check out this commit.
| include(BridgeHelpers) | |
| include(BridgeHelpers OPTIONAL RESULT_VARIABLE BRIDGEHELPERS_MODULE) | |
| if(COMMAND wvg_enable_local_helpers) | |
| wvg_enable_local_helpers() | |
| elseif(BRIDGEHELPERS_MODULE STREQUAL "NOTFOUND") | |
| message(STATUS "BridgeHelpers.cmake not found; skipping wvg_enable_local_helpers().") | |
| endif() |
| target_link_libraries(SharedCode INTERFACE ${_LINK_LIBS}) | ||
| target_link_libraries("${PROJ_NAME}" PRIVATE SharedCode) | ||
| add_dependencies("${PROJ_NAME}" wavgang_webui) | ||
| wvg_enable_local_helpers("${PROJ_NAME}") |
There was a problem hiding this comment.
wvg_enable_local_helpers("${PROJ_NAME}") is called here, but the function isn’t defined anywhere in the repo checkout (outside the cmake submodule). If the submodule revision isn’t updated to include this helper, this will be a hard CMake configure error. Please ensure the required cmake-includes submodule update is included with this change.
| wvg_enable_local_helpers("${PROJ_NAME}") | |
| if(COMMAND wvg_enable_local_helpers) | |
| wvg_enable_local_helpers("${PROJ_NAME}") | |
| endif() |
| for (int i = 0; i < 5; ++i) | ||
| { | ||
| if (isReachable (baseUrl)) | ||
| return; | ||
|
|
||
| if (i > 0) | ||
| juce::Thread::sleep (150); | ||
| } | ||
|
|
||
| auto exe = findBridgeExecutable(); | ||
| if (!exe.existsAsFile()) | ||
| return; | ||
|
|
||
| (void) exe.startAsProcess(); | ||
|
|
||
| for (int i = 0; i < 8; ++i) | ||
| { | ||
| juce::Thread::sleep (150); | ||
| if (isReachable (baseUrl)) | ||
| return; | ||
| } |
There was a problem hiding this comment.
ensureBridgeRunning() is documented as safe to call on the message thread, but this retry loop runs synchronous reachability checks and can block (even before trying to launch the bridge). Since this is called from PluginEditor’s constructor, it can freeze the UI. Consider moving polling off the message thread or making this function fire-and-forget.
| for (int i = 0; i < 5; ++i) | |
| { | |
| if (isReachable (baseUrl)) | |
| return; | |
| if (i > 0) | |
| juce::Thread::sleep (150); | |
| } | |
| auto exe = findBridgeExecutable(); | |
| if (!exe.existsAsFile()) | |
| return; | |
| (void) exe.startAsProcess(); | |
| for (int i = 0; i < 8; ++i) | |
| { | |
| juce::Thread::sleep (150); | |
| if (isReachable (baseUrl)) | |
| return; | |
| } | |
| juce::Thread::launch ([baseUrl] | |
| { | |
| for (int i = 0; i < 5; ++i) | |
| { | |
| if (isReachable (baseUrl)) | |
| return; | |
| if (i > 0) | |
| juce::Thread::sleep (150); | |
| } | |
| auto exe = HostLauncher::findBridgeExecutable(); | |
| if (!exe.existsAsFile()) | |
| return; | |
| (void) exe.startAsProcess(); | |
| for (int i = 0; i < 8; ++i) | |
| { | |
| juce::Thread::sleep (150); | |
| if (isReachable (baseUrl)) | |
| return; | |
| } | |
| }); |
| for (int i = 0; i < 8; ++i) | ||
| { | ||
| juce::Thread::sleep (150); | ||
| if (isReachable (baseUrl)) | ||
| return; |
There was a problem hiding this comment.
This post-launch wait loop sleeps on the calling thread (potentially ~1.2s). If ensureBridgeRunning() is invoked on the message thread (as it currently is), this can cause a visible UI stall. Prefer an async wait (Timer/background thread) or remove the blocking sleep/retry.
| <li :class="{ ok: bridgeLine.includes('OK'), bad: !bridgeLine.includes('OK') }"> | ||
| {{ bridgeLine }} | ||
| </li> |
There was a problem hiding this comment.
The bridge status class is determined by bridgeLine.includes('OK'), which is brittle (changing copy breaks styling) and currently treats the initial "Bridge: …" state as bad. Prefer driving classes from structured state (loading, status.bridge_ok) and adding an explicit waiting state.
| :class="{ | ||
| ok: friendnetLine.includes('connected'), | ||
| bad: friendnetLine.includes('not') || friendnetLine.includes('wrong') || friendnetLine.includes('did not'), | ||
| wait: friendnetLine.includes('Checking'), | ||
| }" |
There was a problem hiding this comment.
FriendNet status styling is based on substring checks against the rendered sentence (e.g. includes('connected'), includes('did not')). This is fragile and can misclassify states if wording changes. Prefer mapping classes directly from status.friendnet.state (and loading) instead of parsing the headline string.
| ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | ||
| defer stop() | ||
|
|
||
| startBundledFriendNet() | ||
| go eng.runPoller(ctx) |
There was a problem hiding this comment.
signal.NotifyContext() intercepts SIGINT/SIGTERM, but the HTTP server is started with http.ListenAndServe and never shut down on <-ctx.Done(). This can prevent Ctrl+C/SIGTERM from stopping the process cleanly. Consider using an http.Server and calling Shutdown() when the context is canceled.
| cmd := exec.Command(exe, "-config", cfgPath, "-nocli") | ||
| cmd.Dir = dataDir | ||
| cmd.Stdout = os.Stdout | ||
| cmd.Stderr = os.Stderr | ||
| if err := cmd.Start(); err != nil { |
There was a problem hiding this comment.
The bridge autostarts friendnet-server as a child process here, but there’s no corresponding shutdown/cleanup on bridge exit. This can leave friendnet-server orphaned after wavgang-bridge stops. Consider tying the child lifecycle to the bridge’s signal context (terminate on shutdown) or otherwise ensuring the subprocess is cleaned up.
|
|
||
| // FriendnetStatus is producer-oriented state for the FriendNet server RPC. | ||
| type FriendnetStatus struct { | ||
| State string `json:"state"` // stopped | starting | reachable | unreachable | misconfigured |
There was a problem hiding this comment.
The doc comment for FriendnetStatus.State doesn’t match the actual states emitted by the implementation/UI/docs (e.g. unknown, auth_required). Update the comment (or constrain the state values) so the type documentation matches the JSON contract.
| State string `json:"state"` // stopped | starting | reachable | unreachable | misconfigured | |
| // State is the JSON contract for FriendNet status: | |
| // unknown | auth_required | stopped | starting | reachable | unreachable | misconfigured | |
| State string `json:"state"` |
| rpc := e.rpcURL | ||
| cli := newRPCClient(rpc) | ||
| fnCtx, cancel := context.WithTimeout(ctx, 4*time.Second) | ||
| defer cancel() | ||
|
|
There was a problem hiding this comment.
refreshFriendnet() constructs a new RPC client (and underlying http.Client) on every poll. Since http.Client manages connection pooling, recreating it repeatedly can cause unnecessary connection churn and GC pressure. Prefer creating/reusing a shared http.Client / RPC client on the statusEngine and reusing it across polls.
Summary
/v1/statusresponsewavgang-bridgeto poll FriendNet over Connect RPC, auto-start a bundledfriendnet-server, and report producer-friendly status valuesTest plan
cd bridge && go build .cd ui && bun run build && bun run testcmake -S . -B build-agent -DCMAKE_BUILD_TYPE=Releasecmake --build build-agent --target WavGang_Standalone -j 4cmake --build build-agent --target WavGang_VST3 -j 4Made with Cursor