-
Notifications
You must be signed in to change notification settings - Fork 0
Add producer-friendly FriendNet bridge status and startup #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,7 @@ include(CPM) | |||||||||
| include(PamplejuceMacOS) | ||||||||||
| include(JUCEDefaults) | ||||||||||
| include(Sanitizers) | ||||||||||
| include(BridgeHelpers) | ||||||||||
|
|
||||||||||
| # ── Read project identity from project.toml ────────────────────── | ||||||||||
| include(ReadProjectConfig) | ||||||||||
|
|
@@ -150,6 +151,7 @@ else() | |||||||||
| 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}") | ||||||||||
|
||||||||||
| wvg_enable_local_helpers("${PROJ_NAME}") | |
| if(COMMAND wvg_enable_local_helpers) | |
| wvg_enable_local_helpers("${PROJ_NAME}") | |
| endif() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,11 @@ | ||
| module github.com/DirektDSP/WavGang/bridge | ||
|
|
||
| go 1.26.2 | ||
|
|
||
| require connectrpc.com/connect v1.19.1 | ||
|
|
||
| require friendnet.org/protocol v0.0.0 | ||
|
|
||
| require google.golang.org/protobuf v1.36.11 // indirect | ||
|
|
||
| replace friendnet.org/protocol => ../third_party/friendnet/protocol |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= | ||
| connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= | ||
| github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= | ||
| github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= | ||
| google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= | ||
| google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "log" | ||
| "net" | ||
| "net/url" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "runtime" | ||
| "strings" | ||
| "sync" | ||
| "time" | ||
| ) | ||
|
|
||
| const defaultServerJSON = `{ | ||
| "listen": ["127.0.0.1:20038"], | ||
| "db_path": "server.db", | ||
| "pem_path": "server.pem", | ||
| "disable_update_checker": true, | ||
| "rpc": { | ||
| "https_pem_path": "rpc.pem", | ||
| "interfaces": [ | ||
| { | ||
| "address": "http://127.0.0.1:18081", | ||
| "allowed_methods": ["GetServerInfo"], | ||
| "cors_allow_all_origins": true | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| ` | ||
|
|
||
| var friendnetProc struct { | ||
| mu sync.Mutex | ||
| cmd *exec.Cmd | ||
| } | ||
|
|
||
| func friendnetExeName() string { | ||
| if runtime.GOOS == "windows" { | ||
| return "friendnet-server.exe" | ||
| } | ||
| return "friendnet-server" | ||
| } | ||
|
|
||
| func findFriendNetServerBinary() string { | ||
| self, err := os.Executable() | ||
| if err != nil { | ||
| return "" | ||
| } | ||
| selfDir := filepath.Dir(self) | ||
| name := friendnetExeName() | ||
| candidates := []string{ | ||
| filepath.Join(selfDir, name), | ||
| filepath.Join(selfDir, "..", name), | ||
| } | ||
| for _, p := range candidates { | ||
| if st, err := os.Stat(p); err == nil && !st.IsDir() { | ||
| return p | ||
| } | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| func ensureFriendNetLayout(dataDir string) error { | ||
| if err := os.MkdirAll(dataDir, 0o700); err != nil { | ||
| return err | ||
| } | ||
| cfgPath := filepath.Join(dataDir, "server.json") | ||
| if _, err := os.Stat(cfgPath); err == nil { | ||
| return nil | ||
| } | ||
| return os.WriteFile(cfgPath, []byte(defaultServerJSON), 0o600) | ||
| } | ||
|
|
||
| func friendnetRPCDialAddr(rpcURL string) string { | ||
| u, err := url.Parse(rpcURL) | ||
| if err != nil || u.Host == "" { | ||
| return "" | ||
| } | ||
| host := u.Hostname() | ||
| port := u.Port() | ||
| if port == "" { | ||
| if strings.EqualFold(u.Scheme, "https") { | ||
| port = "443" | ||
| } else { | ||
| port = "80" | ||
| } | ||
| } | ||
| return net.JoinHostPort(host, port) | ||
| } | ||
|
|
||
| func friendnetRPCReachable(rpcURL string) bool { | ||
| addr := friendnetRPCDialAddr(rpcURL) | ||
| if addr == "" { | ||
| return false | ||
| } | ||
| c, err := net.DialTimeout("tcp", addr, 400*time.Millisecond) | ||
| if err != nil { | ||
| return false | ||
| } | ||
| _ = c.Close() | ||
| return true | ||
| } | ||
|
|
||
| // startBundledFriendNet spawns friendnet-server once if a binary is present. | ||
| func startBundledFriendNet() { | ||
| if strings.EqualFold(os.Getenv("WAVGANG_FRIENDNET_AUTOSTART"), "0") { | ||
| return | ||
| } | ||
| rpc := defaultFriendnetRPCURL() | ||
| if friendnetRPCReachable(rpc) { | ||
| log.Printf("friendnet RPC already reachable at %s; skipping autostart", rpc) | ||
| return | ||
| } | ||
| exe := findFriendNetServerBinary() | ||
| if exe == "" { | ||
| log.Printf("friendnet-server not found beside wavgang-bridge; skipping autostart (set WAVGANG_FRIENDNET_AUTOSTART=0 to silence)") | ||
| return | ||
| } | ||
| dataDir, err := friendnetDataDir() | ||
| if err != nil { | ||
| log.Printf("friendnet data dir: %v", err) | ||
| return | ||
| } | ||
| if err := ensureFriendNetLayout(dataDir); err != nil { | ||
| log.Printf("friendnet layout: %v", err) | ||
| return | ||
| } | ||
|
|
||
| friendnetProc.mu.Lock() | ||
| if friendnetProc.cmd != nil && friendnetProc.cmd.Process != nil { | ||
| friendnetProc.mu.Unlock() | ||
| return | ||
| } | ||
|
|
||
| cfgPath := filepath.Join(dataDir, "server.json") | ||
| cmd := exec.Command(exe, "-config", cfgPath, "-nocli") | ||
| cmd.Dir = dataDir | ||
| cmd.Stdout = os.Stdout | ||
| cmd.Stderr = os.Stderr | ||
| if err := cmd.Start(); err != nil { | ||
|
Comment on lines
+138
to
+142
|
||
| friendnetProc.mu.Unlock() | ||
| log.Printf("start friendnet-server: %v", err) | ||
| return | ||
| } | ||
| friendnetProc.cmd = cmd | ||
| friendnetProc.mu.Unlock() | ||
|
|
||
| log.Printf("started friendnet-server pid=%d workdir=%s", cmd.Process.Pid, dataDir) | ||
|
|
||
| go func(c *exec.Cmd) { | ||
| waitErr := c.Wait() | ||
| friendnetProc.mu.Lock() | ||
| if friendnetProc.cmd == c { | ||
| friendnetProc.cmd = nil | ||
| } | ||
| friendnetProc.mu.Unlock() | ||
| if waitErr != nil && !strings.Contains(waitErr.Error(), "signal") { | ||
| log.Printf("friendnet-server exited: %v", waitErr) | ||
| } | ||
| }(cmd) | ||
|
|
||
| time.Sleep(800 * time.Millisecond) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,15 +2,16 @@ | |
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "log" | ||
| "net/http" | ||
| "os" | ||
| "os/signal" | ||
| "syscall" | ||
| ) | ||
|
|
||
| // withCORS allows the JUCE WebView (Origin juce://juce.backend) to call this loopback API. | ||
| // WebKit often rejects Access-Control-Allow-Origin when echoing a custom-scheme origin; "*" works | ||
| // for simple fetch() without credentials (see ui/src/stores/bridge.ts). | ||
| func withCORS(next http.Handler) http.Handler { | ||
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| w.Header().Set("Access-Control-Allow-Origin", "*") | ||
|
|
@@ -27,19 +28,26 @@ func withCORS(next http.Handler) http.Handler { | |
| } | ||
|
|
||
| func main() { | ||
| log.SetPrefix("wavgang-bridge: ") | ||
| log.SetFlags(0) | ||
|
|
||
| addr := ":17890" | ||
| if v := os.Getenv("WAVGANG_BRIDGE_ADDR"); v != "" { | ||
| addr = v | ||
| } | ||
|
|
||
| rpcURL := defaultFriendnetRPCURL() | ||
| eng := newStatusEngine(rpcURL) | ||
|
|
||
| ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | ||
| defer stop() | ||
|
|
||
| startBundledFriendNet() | ||
| go eng.runPoller(ctx) | ||
|
Comment on lines
+42
to
+46
|
||
|
|
||
| mux := http.NewServeMux() | ||
| mux.HandleFunc("/v1/status", func(w http.ResponseWriter, _ *http.Request) { | ||
| w.Header().Set("Content-Type", "application/json") | ||
| _ = json.NewEncoder(w).Encode(map[string]any{ | ||
| "ok": true, | ||
| "service": "wavgang-bridge", | ||
| "friendnet": "not_connected", | ||
| }) | ||
| writeStatusJSON(w, eng) | ||
| }) | ||
| mux.HandleFunc("/v1/version", func(w http.ResponseWriter, _ *http.Request) { | ||
| w.Header().Set("Content-Type", "application/json") | ||
|
|
@@ -48,7 +56,7 @@ func main() { | |
| }) | ||
| }) | ||
|
|
||
| log.Printf("wavgang-bridge listening on %s", addr) | ||
| log.Printf("wavgang-bridge listening on %s (friendnet RPC %s)", addr, rpcURL) | ||
| if err := http.ListenAndServe(addr, withCORS(mux)); err != nil { | ||
| log.Fatal(err) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
include(BridgeHelpers)/wvg_enable_local_helpers()are introduced, but there is noBridgeHelpers.cmake(orwvg_enable_local_helpersdefinition) in the repo checkout outside thecmakesubmodule. Ensure thecmakesubmodule 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.