mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-30 15:07:49 -04:00
perf(ipc): call Quickshell directly (#3081)
* perf(ipc): call Quickshell directly * fix(ipc): handle indexed responses * fix(ipc): follow error string lint * fix(ipc): fall back to qs when the direct socket call fails --------- Co-authored-by: bbedward <[email protected]>
This commit is contained in:
1 parent
d7b19809aa
commit
c3fd526698
3 files changed
+239
No files matched your search
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/qsipc"
|
||||
)
|
||||
|
||||
type ipcTargets map[string]map[string][]string
|
||||
@@ -137,6 +138,9 @@ func runShellIPCCommand(args []string) {
|
||||
if args[0] != "call" {
|
||||
args = append([]string{"call"}, args...)
|
||||
}
|
||||
if tryDirectIPCCall(args) {
|
||||
return
|
||||
}
|
||||
|
||||
baseArgs, err := buildQsIPCBaseArgs()
|
||||
if err != nil {
|
||||
@@ -153,6 +157,27 @@ func runShellIPCCommand(args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
// tryDirectIPCCall invokes the shell over its IPC socket without spawning qs.
|
||||
// Any failure falls back to the qs child process path, which owns error output.
|
||||
func tryDirectIPCCall(args []string) bool {
|
||||
if len(args) < 3 {
|
||||
return false
|
||||
}
|
||||
pid, ok := shellApp.SessionPID()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
result, isVoid, err := qsipc.Call(qsipc.SocketPathForPID(pid), args[1], args[2], args[3:])
|
||||
if err != nil {
|
||||
log.Debugf("Direct Quickshell IPC failed, falling back to qs: %v", err)
|
||||
return false
|
||||
}
|
||||
if !isVoid {
|
||||
fmt.Println(result)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func printIPCHelp() {
|
||||
fmt.Println("Usage: dms ipc call <target> <function> [args...]")
|
||||
fmt.Println()
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package qsipc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
const (
|
||||
commandStringCall = 3
|
||||
|
||||
// Keep these indexes in sync with quickshell-mirror/quickshell:
|
||||
// https://github.com/quickshell-mirror/quickshell/blob/59e9c47/src/io/ipccomm.cpp#L126
|
||||
responseNotReady = 1
|
||||
responseTargetNotFound = 2
|
||||
responseFunctionNotFound = 3
|
||||
responseArgumentMismatch = 4
|
||||
responseCompleted = 5
|
||||
maxStringLength = 1 << 20
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotReady = errors.New("quickshell IPC not ready")
|
||||
ErrTargetNotFound = errors.New("quickshell IPC target not found")
|
||||
ErrFunctionNotFound = errors.New("quickshell IPC function not found")
|
||||
ErrArgumentMismatch = errors.New("quickshell IPC argument mismatch")
|
||||
ErrUnsupportedResponse = errors.New("unsupported Quickshell IPC response")
|
||||
)
|
||||
|
||||
// Call invokes a Quickshell IpcHandler without starting a qs child process.
|
||||
func Call(socketPath, target, function string, args []string) (string, bool, error) {
|
||||
conn, err := net.DialTimeout("unix", socketPath, 2*time.Second)
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("connect to Quickshell IPC: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(2 * time.Second))
|
||||
if err := writeCall(conn, target, function, args); err != nil {
|
||||
return "", false, fmt.Errorf("write Quickshell IPC request: %w", err)
|
||||
}
|
||||
|
||||
result, isVoid, err := readResponse(bufio.NewReader(conn))
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return result, isVoid, nil
|
||||
}
|
||||
|
||||
func writeCall(w io.Writer, target, function string, args []string) error {
|
||||
var buf bytes.Buffer
|
||||
if err := buf.WriteByte(commandStringCall); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeQString(&buf, target); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeQString(&buf, function); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := binary.Write(&buf, binary.BigEndian, uint32(len(args))); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, arg := range args {
|
||||
if err := writeQString(&buf, arg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err := w.Write(buf.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
func readResponse(r io.Reader) (string, bool, error) {
|
||||
var index uint8
|
||||
if err := binary.Read(r, binary.BigEndian, &index); err != nil {
|
||||
return "", false, fmt.Errorf("read Quickshell IPC response: %w", err)
|
||||
}
|
||||
switch index {
|
||||
case responseNotReady:
|
||||
return "", false, ErrNotReady
|
||||
case responseTargetNotFound:
|
||||
return "", false, ErrTargetNotFound
|
||||
case responseFunctionNotFound:
|
||||
return "", false, ErrFunctionNotFound
|
||||
case responseArgumentMismatch:
|
||||
return "", false, ErrArgumentMismatch
|
||||
case responseCompleted:
|
||||
break
|
||||
default:
|
||||
return "", false, fmt.Errorf("%w: variant index %d", ErrUnsupportedResponse, index)
|
||||
}
|
||||
|
||||
var isVoid uint8
|
||||
if err := binary.Read(r, binary.BigEndian, &isVoid); err != nil {
|
||||
return "", false, fmt.Errorf("read Quickshell IPC completion: %w", err)
|
||||
}
|
||||
value, err := readQString(r)
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("read Quickshell IPC result: %w", err)
|
||||
}
|
||||
return value, isVoid != 0, nil
|
||||
}
|
||||
|
||||
func writeQString(w io.Writer, value string) error {
|
||||
encoded := utf16.Encode([]rune(value))
|
||||
byteLength := len(encoded) * 2
|
||||
if byteLength > maxStringLength {
|
||||
return fmt.Errorf("QString is too large: %d bytes", byteLength)
|
||||
}
|
||||
if err := binary.Write(w, binary.BigEndian, uint32(byteLength)); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, codeUnit := range encoded {
|
||||
if err := binary.Write(w, binary.BigEndian, codeUnit); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readQString(r io.Reader) (string, error) {
|
||||
var length uint32
|
||||
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if length == ^uint32(0) {
|
||||
return "", nil
|
||||
}
|
||||
if length > maxStringLength || length%2 != 0 {
|
||||
return "", fmt.Errorf("invalid QString length: %d bytes", length)
|
||||
}
|
||||
codeUnits := make([]uint16, length/2)
|
||||
for i := range codeUnits {
|
||||
if err := binary.Read(r, binary.BigEndian, &codeUnits[i]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return string(utf16.Decode(codeUnits)), nil
|
||||
}
|
||||
|
||||
// SocketPathForPID returns the instance IPC socket used by qs --pid.
|
||||
func SocketPathForPID(pid int) string {
|
||||
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
|
||||
if runtimeDir == "" {
|
||||
runtimeDir = fmt.Sprintf("/run/user/%d", os.Getuid())
|
||||
}
|
||||
return filepath.Join(runtimeDir, "quickshell", "by-pid", fmt.Sprint(pid), "ipc.sock")
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package qsipc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriteCall(t *testing.T) {
|
||||
var got bytes.Buffer
|
||||
if err := writeCall(&got, "spotlight", "toggle", []string{"true", "é"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want, err := hex.DecodeString("030000001200730070006f0074006c00690067006800740000000c0074006f00670067006c0065000000020000000800740072007500650000000200e9")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got.Bytes(), want) {
|
||||
t.Fatalf("unexpected wire bytes:\ngot %x\nwant %x", got.Bytes(), want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadResponse(t *testing.T) {
|
||||
data, err := hex.DecodeString("050000000018007b0022006f006b0022003a00200074007200750065007d")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
value, isVoid, err := readResponse(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if isVoid || value != `{"ok": true}` {
|
||||
t.Fatalf("unexpected response: value=%q void=%v", value, isVoid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadResponseErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
index byte
|
||||
want error
|
||||
}{
|
||||
{name: "not ready", index: responseNotReady, want: ErrNotReady},
|
||||
{name: "target not found", index: responseTargetNotFound, want: ErrTargetNotFound},
|
||||
{name: "function not found", index: responseFunctionNotFound, want: ErrFunctionNotFound},
|
||||
{name: "argument mismatch", index: responseArgumentMismatch, want: ErrArgumentMismatch},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, _, err := readResponse(bytes.NewReader([]byte{tt.index}))
|
||||
if !errors.Is(err, tt.want) {
|
||||
t.Fatalf("readResponse() error = %v, want %v", err, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user