Files
bjarneo--cliamp/ipc/client.go
T
Faza Iman Imron f373776d4c feat(plugins): add retained event pub/sub over IPC (#303)
* feat(ipc): add retained event subscriptions

* feat(plugins): expose namespaced event publishing

* feat(app): wire plugin events to IPC

* docs(plugins): document local event pubsub

* fix(ipc): clear request deadline for subscriptions

* fix(luaplugin): install event publisher before plugins load

* fix(luaplugin): clear retained events after publishers stop

* fix(ipc): keep event sequence gap-free when retention is rejected

* fix(luaplugin): break table cycles and cap depth in Lua value conversion

* docs(plugins): document conversion limits and subscription stream rules

* refactor(ipc): report a missing server with a sentinel error

* fix(luaplugin): fold event namespace into one topic segment

* docs(site): mention plugin event publishing and subscriptions

* fix(luaplugin): keep event namespaces unique across plugins

* fix(luaplugin): release event namespace by installed filename

* refactor(ipc): render the not-running message in the command layer

* docs(plugins): clarify namespace prefixes and JSON conversion limits
2026-08-18 17:19:28 +02:00

69 lines
1.9 KiB
Go

package ipc
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/bjarneo/cliamp/internal/appdir"
)
// DefaultSocketPath returns the default IPC socket path (~/.config/cliamp/cliamp.sock).
func DefaultSocketPath() string {
dir, err := appdir.Dir()
if err != nil {
return filepath.Join(os.TempDir(), "cliamp.sock")
}
return filepath.Join(dir, "cliamp.sock")
}
// Send connects to the IPC socket, sends a request, and returns the response.
// The connection is closed after a single request/response exchange.
func Send(sockPath string, req Request) (Response, error) {
return SendWithDeadline(sockPath, req, 5*time.Second)
}
// SendWithDeadline is like Send but lets the caller override the exchange
// deadline. Plugin commands can legitimately run for minutes (downloads), so
// the generic 5s cap is too short for them.
func SendWithDeadline(sockPath string, req Request, deadline time.Duration) (Response, error) {
conn, err := dialSocket(sockPath, 3*time.Second)
if err != nil {
if isSocketUnavailable(err) {
return Response{}, fmt.Errorf("no socket at %s: %w", sockPath, ErrNotRunning)
}
return Response{}, fmt.Errorf("connect: %w", err)
}
defer conn.Close()
conn.SetDeadline(time.Now().Add(deadline))
// Encode and send the request as a single JSON line.
data, err := json.Marshal(req)
if err != nil {
return Response{}, fmt.Errorf("marshal request: %w", err)
}
data = append(data, '\n')
if _, err := conn.Write(data); err != nil {
return Response{}, fmt.Errorf("write: %w", err)
}
// Read the response line.
scanner := bufio.NewScanner(conn)
if !scanner.Scan() {
if err := scanner.Err(); err != nil {
return Response{}, fmt.Errorf("read response: %w", err)
}
return Response{}, fmt.Errorf("no response from server")
}
var resp Response
if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil {
return Response{}, fmt.Errorf("unmarshal response: %w", err)
}
return resp, nil
}