Files
2026-10-20 12:23:25 -04:00

142 lines
3.6 KiB
Go

package main
import (
"bufio"
"encoding/json"
"fmt"
"io"
"net"
"os"
"path/filepath"
"strings"
"time"
"github.com/duxlabs-dev/dux-go/ipc"
"github.com/duxlabs-dev/dux-shell/core/internal/server"
"github.com/duxlabs-dev/dux-shell/core/internal/utils"
)
// maxIPCMessageSize allows room for a 50 MB clipboard entry plus JSON/base64
// overhead in the line-delimited IPC response.
const maxIPCMessageSize = 96 * 1024 * 1024
// serverHandshakeTimeout bounds connecting, the greeting and sending the
// request, so a stuck server can't hang the CLI. Replies get no limit because
// some requests (matugen queue --wait) legitimately take long.
var serverHandshakeTimeout = 2 * time.Second
func dialServer() (net.Conn, *bufio.Scanner, error) {
conn, err := net.DialTimeout("unix", getServerSocketPath(), serverHandshakeTimeout)
if err != nil {
return nil, nil, fmt.Errorf("failed to connect to server (is it running?): %w", err)
}
_ = conn.SetDeadline(time.Now().Add(serverHandshakeTimeout))
scanner := bufio.NewScanner(conn)
scanner.Buffer(make([]byte, bufio.MaxScanTokenSize), maxIPCMessageSize)
if !scanner.Scan() { // initial capabilities message
conn.Close()
err := scanner.Err()
if err == nil {
err = io.EOF
}
return nil, nil, fmt.Errorf("server did not respond: %w", err)
}
return conn, scanner, nil
}
func sendServerRequest(req ipc.Request) (*ipc.Response[any], error) {
conn, scanner, err := dialServer()
if err != nil {
return nil, err
}
defer conn.Close()
reqData, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
if _, err := conn.Write(reqData); err != nil {
return nil, fmt.Errorf("failed to write request: %w", err)
}
if _, err := conn.Write([]byte("\n")); err != nil {
return nil, fmt.Errorf("failed to write newline: %w", err)
}
_ = conn.SetDeadline(time.Time{})
if !scanner.Scan() {
return nil, fmt.Errorf("failed to read response")
}
var resp ipc.Response[any]
if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &resp, nil
}
// sendServerRequestFireAndForget sends a request without waiting for a response.
// Useful for commands that trigger UI or async operations.
func sendServerRequestFireAndForget(req ipc.Request) error {
conn, _, err := dialServer()
if err != nil {
return err
}
defer conn.Close()
reqData, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
if _, err := conn.Write(reqData); err != nil {
return fmt.Errorf("failed to write request: %w", err)
}
if _, err := conn.Write([]byte("\n")); err != nil {
return fmt.Errorf("failed to write newline: %w", err)
}
return nil
}
// tryServerRequest attempts to send a request but returns false if server unavailable.
// Does not log errors - caller can decide what to do on failure.
func tryServerRequest(req ipc.Request) (*ipc.Response[any], bool) {
resp, err := sendServerRequest(req)
if err != nil {
return nil, false
}
return resp, true
}
func getServerSocketPath() string {
runtimeDir := utils.RuntimeDir()
if sessionSock, ok := shellApp.SessionSocketPath(); ok {
return sessionSock
}
entries, err := os.ReadDir(runtimeDir)
if err != nil {
return server.GetSocketPath()
}
for _, id := range []string{"dms", "danklinux"} {
for _, entry := range entries {
name := entry.Name()
if filepath.Ext(name) != ".sock" {
continue
}
if name == id+".sock" || strings.HasPrefix(name, id+"-") {
return filepath.Join(runtimeDir, name)
}
}
}
return server.GetSocketPath()
}