Answer omarchy-shell calls over the shell's own socket (#13435)
* Answer omarchy-shell calls over the shell's own socket Every omarchy-shell call started a qs ipc client, ~45ms of startup for one IPC call: a theme switch makes two, and every script-driven OSD, toggle refresh and lock query paid it too. The shell now serves a socket in XDG_RUNTIME_DIR, named from its config path and Wayland display as qs ipc selects its instance, and omarchy-shell tries it first through socat, which starts in ~5ms. First-party handlers register as ShellIpc, an IpcHandler that qs ipc still reaches, and the socket calls only the functions a handler declares with their exact argument count, allowed by name so QObject methods such as destroy() stay out of reach. When the shell ran nothing it answers SKIP, and omarchy-shell asks qs ipc for its exact answer, so errors, third-party plugins and an unreachable socket behave as before. A call that may have run is never retried: a timeout or a connection closed without an answer reports the shell as not responding. omarchy-shell shell ping takes ~13-18ms instead of ~61ms, and omarchy-osd reaches the screen in ~36ms instead of ~77ms. Output and exit status match the qs ipc path across 26 calls, errors and quiet mode included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Only accept whole socket replies and retry only unmade connections A reply cut off after its OK prefix passed for the whole answer, and an empty reply with socat failing was retried through qs ipc although the request might already have been delivered. An answer now counts only once its record separator arrived. socat's own errors join the reply, so only its connect error, a socket nothing listens on, falls back to qs ipc beside an explicit SKIP; anything else is reported as not responding rather than retried. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
616feda103
commit
c231097df7
27 files changed
+356
-23
No files matched your search
@@ -52,6 +52,50 @@ if [[ $1 == "shell" && ( $2 == "summon" || $2 == "toggle" ) ]] && (( $# == 3 ));
|
||||
set -- "$1" "$2" "$3" "{}"
|
||||
fi
|
||||
|
||||
# The shell answers on its own socket first: socat starts in ~5ms where a qs
|
||||
# ipc client takes ~45ms. Fields are split by unit separators and the request
|
||||
# ends with a record separator, so an argument holding either goes through qs
|
||||
# ipc instead. The shell replies "OK" with the output once the call has run,
|
||||
# or "SKIP" when it ran nothing (no such target or function, the wrong number
|
||||
# of arguments), and qs ipc then gives its exact answer. A timeout after the
|
||||
# request is out is reported, not retried: the call may already have run.
|
||||
# Like qs ipc, the socket belongs to one shell, the one running this config on
|
||||
# this display, and the shell derives its name from the same two values.
|
||||
socket_id=$(printf '%s\n%s' "$OMARCHY_PATH/shell" "${WAYLAND_DISPLAY:-}" | md5sum)
|
||||
socket="${XDG_RUNTIME_DIR:-/run/user/$UID}/omarchy-shell-${socket_id:0:16}.sock"
|
||||
request="$*"
|
||||
if [[ -S $socket && $request != *$'\x1e'* && $request != *$'\x1f'* ]]; then
|
||||
saved_ifs=$IFS
|
||||
IFS=$'\x1f'
|
||||
request="$*"
|
||||
IFS=$saved_ifs
|
||||
# socat waits as long as the shell takes; the timeout alone bounds the call.
|
||||
# Its own errors land in the reply too, so a connection it could not make
|
||||
# is told apart from one that failed after the request went out.
|
||||
reply=$(printf '%s\x1e' "$request" | timeout --kill-after=1s "${OMARCHY_SHELL_IPC_TIMEOUT:-2s}" socat -t 86400 - "UNIX-CONNECT:$socket" 2>&1)
|
||||
|
||||
# An answer counts only once its record separator arrived: a reply cut off
|
||||
# partway must not pass for the whole of it.
|
||||
if [[ $reply == OK$'\x1f'*$'\x1e'* ]]; then
|
||||
output=${reply#OK$'\x1f'}
|
||||
output=${output%$'\x1e'*}
|
||||
# Match qs ipc's path, whose $(...) drops trailing newlines.
|
||||
while [[ $output == *$'\n' ]]; do output=${output%$'\n'}; done
|
||||
if (( !QUIET )) && [[ -n $output ]]; then
|
||||
echo "$output"
|
||||
fi
|
||||
exit 0
|
||||
elif [[ $reply == "SKIP"$'\x1e'* ]]; then
|
||||
: # The shell ran nothing; qs ipc gives its exact answer below.
|
||||
elif [[ $reply != OK* && $reply == *" E connect("* ]]; then
|
||||
: # socat never connected, a stale socket; qs ipc answers below.
|
||||
else
|
||||
# Out of time, cut off, or failed after the request went out: the call
|
||||
# may have run, so it is reported rather than retried.
|
||||
fail "omarchy-shell is not responding"
|
||||
fi
|
||||
fi
|
||||
|
||||
# The -- keeps function names that shadow qs subcommands (e.g. show) as
|
||||
# positionals. qs reports connection failures with a nonzero exit, but IPC-level
|
||||
# failures (unknown target/function, bad arguments) go to stdout with exit 0.
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
// Every ShellIpc handler, so the shell can answer an omarchy-shell call over
|
||||
// its own socket instead of through a qs ipc client started per call. It
|
||||
// answers only what qs ipc would: the first live, enabled handler for a
|
||||
// target, and only the functions that handler declares. Those are allowed by
|
||||
// name, never inferred from what a function call happens to reach: QObject
|
||||
// methods such as destroy() are callable but not enumerable.
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property var handlers: []
|
||||
|
||||
// A bare handler's own functions (signals and hooks such as targetChanged),
|
||||
// which qs ipc never exposes and neither may the socket.
|
||||
readonly property var builtins: functionNames(bareHandler)
|
||||
|
||||
IpcHandler {
|
||||
id: bareHandler
|
||||
enabled: false
|
||||
}
|
||||
|
||||
function functionNames(object) {
|
||||
var names = []
|
||||
for (var key in object) {
|
||||
if (typeof object[key] === "function") names.push(key)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
function register(handler) {
|
||||
if (handlers.indexOf(handler) === -1) handlers.push(handler)
|
||||
}
|
||||
|
||||
function unregister(handler) {
|
||||
var index = handlers.indexOf(handler)
|
||||
if (index !== -1) handlers.splice(index, 1)
|
||||
}
|
||||
|
||||
function handlerFor(target) {
|
||||
for (var i = 0; i < handlers.length; i++) {
|
||||
var handler = handlers[i]
|
||||
if (handler && handler.enabled && handler.target === target) return handler
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// { ran: true, output } once the function ran; { ran: false } when it did
|
||||
// not (unknown target or function, wrong argument count), so the caller
|
||||
// can ask qs ipc for its exact answer without running anything twice.
|
||||
// The functions a handler declares: what it enumerates beyond a bare
|
||||
// handler, less property change signals.
|
||||
function declaredFunctions(handler) {
|
||||
return functionNames(handler).filter(function(name) {
|
||||
return builtins.indexOf(name) === -1 && !/Changed$/.test(name)
|
||||
})
|
||||
}
|
||||
|
||||
function call(target, method, args) {
|
||||
var handler = handlerFor(target)
|
||||
if (!handler) return { ran: false }
|
||||
if (declaredFunctions(handler).indexOf(method) === -1) return { ran: false }
|
||||
if (handler[method].length !== args.length) return { ran: false }
|
||||
|
||||
var result
|
||||
try {
|
||||
result = handler[method].apply(handler, args)
|
||||
} catch (error) {
|
||||
console.warn("ipc " + target + " " + method + " failed: " + error)
|
||||
return { ran: true, output: "" }
|
||||
}
|
||||
return { ran: true, output: result === undefined || result === null ? "" : String(result) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
|
||||
// An IpcHandler the shell can also answer over its own socket (see
|
||||
// IpcRegistry), so omarchy-shell reaches it without a qs ipc client. qs ipc
|
||||
// still reaches it as before.
|
||||
IpcHandler {
|
||||
id: handler
|
||||
|
||||
Component.onCompleted: IpcRegistry.register(handler)
|
||||
Component.onDestruction: IpcRegistry.unregister(handler)
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
module qs.Commons
|
||||
singleton Border 1.0 Border.qml
|
||||
singleton Color 1.0 Color.qml
|
||||
singleton IpcRegistry 1.0 IpcRegistry.qml
|
||||
singleton Style 1.0 Style.qml
|
||||
singleton Util 1.0 Util.qml
|
||||
ShellIpc 1.0 ShellIpc.qml
|
||||
+1
-1
@@ -45,7 +45,7 @@ Item {
|
||||
id: panelController
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
enabled: root.manageIpc && root.ipcTarget !== ""
|
||||
target: root.ipcTarget
|
||||
|
||||
|
||||
@@ -324,7 +324,7 @@ Panel {
|
||||
onTriggered: root.nowMs = Date.now()
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: root.ipcTarget
|
||||
function open(): void { root.open() }
|
||||
function close(): void { root.close() }
|
||||
|
||||
@@ -208,7 +208,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: "background"
|
||||
|
||||
function refresh(): void {
|
||||
|
||||
@@ -1182,7 +1182,7 @@ Item {
|
||||
// changes land in quick succession, stranding the bar off screen until the
|
||||
// shell restarts. `omarchy-toggle-bar` nudges this after flipping the flag
|
||||
// so the probe re-reads it even when the watch has gone quiet.
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: "omarchy.bar"
|
||||
|
||||
// Start rather than restart: a probe already in flight was launched by the
|
||||
|
||||
@@ -165,7 +165,7 @@ BarWidget {
|
||||
? activeVerticalBlock.implicitHeight + inactiveVerticalArea.implicitHeight
|
||||
: Math.max(activeHorizontalBlock.implicitHeight, inactiveHorizontalArea.implicitHeight)
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: "omarchy.indicators"
|
||||
|
||||
function refresh(): void {
|
||||
|
||||
@@ -24,7 +24,7 @@ BarWidget {
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: "omarchy.system-update"
|
||||
|
||||
function refresh(): void {
|
||||
|
||||
@@ -641,7 +641,7 @@ Item {
|
||||
checkStrandedLock()
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: "lock"
|
||||
|
||||
function lock(): string {
|
||||
|
||||
@@ -862,7 +862,7 @@ Item {
|
||||
return true
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
id: ipcHandler
|
||||
target: "notifications"
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ Item {
|
||||
text: OsdModel.widestIcon
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: "osd"
|
||||
function show(payloadJson: string): string {
|
||||
root.open(payloadJson)
|
||||
|
||||
@@ -637,7 +637,7 @@ Panel {
|
||||
Quickshell.execDetached(["omarchy-bluetooth-power", adapter.enabled ? "off" : "on"])
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: "omarchy.bluetooth"
|
||||
|
||||
function open() { root.open() }
|
||||
|
||||
@@ -130,7 +130,7 @@ BarWidget {
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: "omarchy.clock"
|
||||
|
||||
function refresh(): void { root.broadcast("refresh") }
|
||||
|
||||
@@ -155,7 +155,7 @@ Panel {
|
||||
function onFilesChanged() { root.ensureCursor() }
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: root.ipcTarget
|
||||
function open(): void { root.open() }
|
||||
function close(): void { root.close() }
|
||||
|
||||
@@ -217,7 +217,7 @@ Panel {
|
||||
})
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: "omarchy.monitor"
|
||||
|
||||
function brightness(percent: string): string { return root.brightnessIpc(percent) }
|
||||
|
||||
@@ -207,7 +207,7 @@ Panel {
|
||||
Qt.callLater(function() { root.refresh(true) })
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: "omarchy.network"
|
||||
|
||||
function open() { root.open() }
|
||||
|
||||
@@ -174,7 +174,7 @@ Panel {
|
||||
if (root.bar && root.bar.shell) root.bar.shell.updateEntryInline(root.moduleName, root.settings)
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: "omarchy.power"
|
||||
|
||||
function open() { root.open() }
|
||||
|
||||
@@ -363,7 +363,7 @@ Panel {
|
||||
function onAccountsAccessDeniedChanged() { root.ensureCursor() }
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: root.ipcTarget
|
||||
function open(): void { root.open() }
|
||||
function close(): void { root.close() }
|
||||
|
||||
@@ -475,7 +475,7 @@ Panel {
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: root.ipcTarget
|
||||
|
||||
function open(): void { root.openFromHotkey() }
|
||||
|
||||
@@ -4,6 +4,7 @@ import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import "IdleModel.js" as IdleModel
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -335,7 +336,7 @@ Item {
|
||||
refreshStayAwakeState()
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: "idle"
|
||||
|
||||
function status(): string {
|
||||
|
||||
@@ -4,6 +4,7 @@ import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Services.Pipewire
|
||||
import "MediaModel.js" as MediaModel
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -530,7 +531,7 @@ Item {
|
||||
return true
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
id: ipcHandler
|
||||
target: "media"
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "NightlightModel.js" as NightlightModel
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -85,7 +86,7 @@ Item {
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: "nightlight"
|
||||
|
||||
function status(): string {
|
||||
|
||||
+36
-2
@@ -1579,6 +1579,40 @@ ShellRoot {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ IPC socket
|
||||
//
|
||||
// omarchy-shell reaches the shell here first: a qs ipc client costs ~45ms
|
||||
// to start per call, socat ~5ms. A request is target, method and arguments
|
||||
// separated by unit separators and ended by a record separator. The reply is
|
||||
// "OK" and the output, or "SKIP" when nothing ran (no such target or
|
||||
// function, or the wrong number of arguments), which omarchy-shell hands to
|
||||
// qs ipc for its exact answer. The socket sits in XDG_RUNTIME_DIR, private
|
||||
// to the user like qs ipc's own. Like qs ipc, it belongs to one shell: the
|
||||
// one running this config on this display. omarchy-shell derives the same
|
||||
// name from the same two values.
|
||||
readonly property string ipcSocketPath: Quickshell.env("XDG_RUNTIME_DIR") + "/omarchy-shell-"
|
||||
+ Qt.md5(shell.omarchyPath + "/shell\n" + Quickshell.env("WAYLAND_DISPLAY")).slice(0, 16) + ".sock"
|
||||
|
||||
SocketServer {
|
||||
active: shell.omarchyPath !== ""
|
||||
path: shell.ipcSocketPath
|
||||
|
||||
handler: Socket {
|
||||
id: connection
|
||||
|
||||
parser: SplitParser {
|
||||
splitMarker: "\u001e"
|
||||
onRead: function(data) {
|
||||
var fields = String(data).split("\u001f")
|
||||
var result = fields.length >= 2 ? IpcRegistry.call(fields[0], fields[1], fields.slice(2)) : { ran: false }
|
||||
connection.write(result.ran ? "OK\u001f" + result.output + "\u001e" : "SKIP\u001e")
|
||||
connection.flush()
|
||||
connection.connected = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------- image selector IPC
|
||||
|
||||
function imagePickerItem() {
|
||||
@@ -1586,7 +1620,7 @@ ShellRoot {
|
||||
return loader && loader.item ? loader.item : null
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: "image-selector"
|
||||
|
||||
function open(imageDirs: string,
|
||||
@@ -1637,7 +1671,7 @@ ShellRoot {
|
||||
|
||||
// ---------------------------------------------------------- shell IPC
|
||||
|
||||
IpcHandler {
|
||||
ShellIpc {
|
||||
target: "shell"
|
||||
|
||||
function ping(): string {
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
require_command socat
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
server_pid=""
|
||||
cleanup() {
|
||||
[[ -n $server_pid ]] && kill "$server_pid" 2>/dev/null || true
|
||||
rm -rf "$test_tmp"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
root_dir="$test_tmp/root"
|
||||
run_dir="$test_tmp/run"
|
||||
stub_bin="$test_tmp/bin"
|
||||
mkdir -p "$root_dir/shell" "$run_dir" "$stub_bin"
|
||||
touch "$root_dir/shell/shell.qml"
|
||||
# The shell names its socket after its config and display, as omarchy-shell
|
||||
# derives it.
|
||||
display="test-display"
|
||||
socket_id=$(printf '%s\n%s' "$root_dir/shell" "$display" | md5sum)
|
||||
socket="$run_dir/omarchy-shell-${socket_id:0:16}.sock"
|
||||
|
||||
# qs is the fallback: it records that it ran and answers.
|
||||
cat >"$stub_bin/qs" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'qs\n' >>"$QS_CALLS"
|
||||
printf 'from-qs\n'
|
||||
SH
|
||||
chmod +x "$stub_bin/qs"
|
||||
|
||||
# The fake shell replies as the mode file says, and logs each request with
|
||||
# its separators made visible.
|
||||
cat >"$test_tmp/responder" <<'SH'
|
||||
#!/bin/bash
|
||||
IFS= read -r -d $'\x1e' request
|
||||
printf '%s\n' "${request//$'\x1f'/|}" >>"$REQUESTS"
|
||||
case $(<"$MODE") in
|
||||
ok) printf 'OK\x1f%s\x1e' "answer to ${request//$'\x1f'/ }" ;;
|
||||
newlines) printf 'OK\x1fline\n\n\x1e' ;;
|
||||
skip) printf 'SKIP\x1e' ;;
|
||||
hang) sleep 2 ;;
|
||||
close) ;;
|
||||
partial) printf 'OK\x1fpartial answer' ;;
|
||||
esac
|
||||
SH
|
||||
chmod +x "$test_tmp/responder"
|
||||
|
||||
mode="$test_tmp/mode"
|
||||
calls="$test_tmp/qs-calls"
|
||||
requests="$test_tmp/requests"
|
||||
MODE="$mode" REQUESTS="$requests" socat UNIX-LISTEN:"$socket",fork SYSTEM:"$test_tmp/responder" 2>/dev/null &
|
||||
server_pid=$!
|
||||
for _ in $(seq 50); do [[ -S $socket ]] && break; sleep 0.05; done
|
||||
|
||||
shell_call() {
|
||||
: >"$calls"
|
||||
PATH="$stub_bin:$PATH" OMARCHY_PATH="$root_dir" XDG_RUNTIME_DIR="$run_dir" WAYLAND_DISPLAY="$display" QS_CALLS="$calls" \
|
||||
OMARCHY_SHELL_IPC_TIMEOUT="${IPC_TIMEOUT:-2s}" "$ROOT/bin/omarchy-shell" "$@"
|
||||
}
|
||||
|
||||
printf 'ok' >"$mode"
|
||||
output=$(shell_call media next extra-arg)
|
||||
[[ $output == "answer to media next extra-arg" ]] || fail "the shell answers over its socket" "got: $output"
|
||||
[[ ! -s $calls ]] || fail "an answered call never starts qs ipc"
|
||||
[[ $(tail -n1 "$requests") == "media|next|extra-arg" ]] || fail "arguments travel as separate fields" "got: $(tail -n1 "$requests")"
|
||||
pass "the shell answers over its socket without starting qs ipc"
|
||||
|
||||
output=$(shell_call -q media next)
|
||||
[[ -z $output ]] || fail "quiet calls print nothing" "got: $output"
|
||||
pass "quiet calls over the socket print nothing"
|
||||
|
||||
printf 'newlines' >"$mode"
|
||||
output=$(shell_call shell ping)
|
||||
[[ $output == "line" ]] || fail "socket output drops trailing newlines as qs ipc's does" "got: $(printf '%q' "$output")"
|
||||
pass "socket output drops trailing newlines as the qs ipc path does"
|
||||
|
||||
printf 'skip' >"$mode"
|
||||
output=$(shell_call nosuch ping)
|
||||
[[ $output == "from-qs" && -s $calls ]] || fail "a call the shell did not run goes to qs ipc" "got: $output"
|
||||
pass "a call the shell did not run goes to qs ipc for its exact answer"
|
||||
|
||||
printf 'ok' >"$mode"
|
||||
: >"$requests"
|
||||
output=$(shell_call shell summon "$(printf 'a\x1fb')")
|
||||
[[ $output == "from-qs" && ! -s $requests ]] || fail "an argument holding a separator goes straight to qs ipc" "got: $output"
|
||||
pass "an argument holding a separator goes straight to qs ipc"
|
||||
|
||||
printf 'hang' >"$mode"
|
||||
output=$(IPC_TIMEOUT=0.5s shell_call shell ping 2>&1) && fail "a shell that never replies fails the call"
|
||||
[[ $output == "omarchy-shell is not responding" && ! -s $calls ]] ||
|
||||
fail "a call that may have run is reported, not retried through qs ipc" "got: $output"
|
||||
pass "a call that may have run is reported as unresponsive, not retried"
|
||||
|
||||
printf 'close' >"$mode"
|
||||
output=$(shell_call shell ping 2>&1) && fail "a shell that closes without answering fails the call"
|
||||
[[ $output == "omarchy-shell is not responding" && ! -s $calls ]] ||
|
||||
fail "a connection closed without an answer is reported, not retried" "got: $output"
|
||||
pass "a connection closed without an answer is reported, not retried"
|
||||
|
||||
printf 'partial' >"$mode"
|
||||
output=$(shell_call shell listPlugins 2>&1) && fail "a reply cut off before its end fails the call"
|
||||
[[ $output == "omarchy-shell is not responding" && ! -s $calls ]] ||
|
||||
fail "a reply cut off before its end is reported, not taken or retried" "got: $output"
|
||||
pass "a reply cut off before its end is reported, not taken or retried"
|
||||
|
||||
# SIGKILL leaves the socket file behind with nothing listening, as a crashed
|
||||
# shell does; socat then never connects, so qs ipc may safely answer.
|
||||
kill -9 "$server_pid"
|
||||
wait "$server_pid" 2>/dev/null || true
|
||||
server_pid=""
|
||||
[[ -S $socket ]] || fail "a killed listener leaves its socket file behind"
|
||||
output=$(shell_call shell ping)
|
||||
[[ $output == "from-qs" ]] || fail "a stale socket with nothing listening falls back to qs ipc" "got: $output"
|
||||
pass "a stale socket with nothing listening falls back to qs ipc"
|
||||
|
||||
rm -f "$socket"
|
||||
output=$(shell_call shell ping)
|
||||
[[ $output == "from-qs" ]] || fail "without the socket calls go through qs ipc" "got: $output"
|
||||
pass "without the socket calls go through qs ipc"
|
||||
|
||||
# A call for a different display never reaches this shell's socket.
|
||||
printf 'ok' >"$mode"
|
||||
MODE="$mode" REQUESTS="$requests" socat UNIX-LISTEN:"$socket",fork SYSTEM:"$test_tmp/responder" 2>/dev/null &
|
||||
server_pid=$!
|
||||
for _ in $(seq 50); do [[ -S $socket ]] && break; sleep 0.05; done
|
||||
output=$(display="other-display" shell_call shell ping)
|
||||
[[ $output == "from-qs" ]] || fail "a call for another display does not reach this shell's socket" "got: $output"
|
||||
pass "a call for another display does not reach this shell's socket"
|
||||
|
||||
run_node_test <<'JS'
|
||||
const fs = require('fs')
|
||||
const { execSync } = require('child_process')
|
||||
const shellQml = fs.readFileSync(path.join(root, 'shell/shell.qml'), 'utf8')
|
||||
const registry = fs.readFileSync(path.join(root, 'shell/Commons/IpcRegistry.qml'), 'utf8')
|
||||
|
||||
// Only handlers that register can answer over the socket, so every
|
||||
// first-party one does; the registry's own bare handler is the reference for
|
||||
// the functions a handler never exposes.
|
||||
const bare = execSync(`grep -rln 'IpcHandler {' ${path.join(root, 'shell')} --include=*.qml || true`).toString().trim().split('\n').filter(Boolean)
|
||||
assertDeepEqual(bare.map((f) => path.relative(root, f)).sort(), ['shell/Commons/IpcRegistry.qml', 'shell/Commons/ShellIpc.qml'], 'first-party IPC handlers register through ShellIpc')
|
||||
|
||||
// Declared functions are allowed by name: destroy() and other QObject
|
||||
// methods are callable yet not enumerable, so subtracting builtins from what a
|
||||
// call can reach would let them through.
|
||||
assert(
|
||||
registry.includes('readonly property var builtins: functionNames(bareHandler)') &&
|
||||
/return builtins\.indexOf\(name\) === -1 && !\/Changed\$\/\.test\(name\)/.test(registry) &&
|
||||
registry.includes('if (declaredFunctions(handler).indexOf(method) === -1) return { ran: false }') &&
|
||||
registry.includes('handler[method].length !== args.length'),
|
||||
'the socket answers only declared functions with their exact argument count'
|
||||
)
|
||||
assert(
|
||||
shellQml.includes('+ Qt.md5(shell.omarchyPath + "/shell\\n" + Quickshell.env("WAYLAND_DISPLAY")).slice(0, 16) + ".sock"'),
|
||||
'the shell names its socket after its config and display, as omarchy-shell does'
|
||||
)
|
||||
JS
|
||||
@@ -121,7 +121,7 @@ grep -q 'paths.omarchy_path .. "/default/omarchy/shortcuts"' "$ROOT/default/hypr
|
||||
for service in services/media notifications; do
|
||||
grep -Pzq 'function runShortcut\(method\) \{\n if \(typeof ipcHandler\[method\] !== "function"\) return false\n ipcHandler\[method\]\(\)' "$ROOT/shell/plugins/$service/Service.qml" ||
|
||||
fail "$service runs ipc shortcuts through its IPC handler"
|
||||
grep -Pzq 'IpcHandler \{\n id: ipcHandler' "$ROOT/shell/plugins/$service/Service.qml" ||
|
||||
grep -Pzq '(IpcHandler|ShellIpc) \{\n id: ipcHandler' "$ROOT/shell/plugins/$service/Service.qml" ||
|
||||
fail "$service names its IPC handler for shortcuts"
|
||||
done
|
||||
pass "the shell and the helpers share one shortcut list"
|
||||
|
||||
Reference in new issue
Block a user