1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-07 05:58:28 -04:00

Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell

This commit is contained in:
bbedward
2026-07-13 16:03:56 -04:00
32 changed files with 587 additions and 169 deletions
+18 -1
View File
@@ -133,6 +133,11 @@ func mergedAssociations() *MimeAssociations {
return merged return merged
} }
// isSafeIniField rejects values that would corrupt a key=value line in mimeapps.list
func isSafeIniField(s string) bool {
return !strings.ContainsAny(s, "\n\r[]")
}
func writeUserMimeapps(update func(*MimeAssociations)) error { func writeUserMimeapps(update func(*MimeAssociations)) error {
mimeappsWriteMu.Lock() mimeappsWriteMu.Lock()
defer mimeappsWriteMu.Unlock() defer mimeappsWriteMu.Unlock()
@@ -152,6 +157,7 @@ func writeUserMimeapps(update func(*MimeAssociations)) error {
var buf bytes.Buffer var buf bytes.Buffer
w := bufio.NewWriter(&buf) w := bufio.NewWriter(&buf)
var writeErr error
writeSection := func(name string, entries map[string]string) { writeSection := func(name string, entries map[string]string) {
fmt.Fprintf(w, "[%s]\n", name) fmt.Fprintf(w, "[%s]\n", name)
keys := make([]string, 0, len(entries)) keys := make([]string, 0, len(entries))
@@ -160,7 +166,14 @@ func writeUserMimeapps(update func(*MimeAssociations)) error {
} }
sort.Strings(keys) sort.Strings(keys)
for _, k := range keys { for _, k := range keys {
fmt.Fprintf(w, "%s=%s\n", k, entries[k]) v := entries[k]
if !isSafeIniField(k) || !isSafeIniField(v) {
if writeErr == nil {
writeErr = fmt.Errorf("invalid mimeapps.list field %q=%q", k, v)
}
continue
}
fmt.Fprintf(w, "%s=%s\n", k, v)
} }
fmt.Fprintln(w) fmt.Fprintln(w)
} }
@@ -177,6 +190,10 @@ func writeUserMimeapps(update func(*MimeAssociations)) error {
writeSection(groupAdded, flatten(assoc.Added)) writeSection(groupAdded, flatten(assoc.Added))
writeSection(groupRemoved, flatten(assoc.Removed)) writeSection(groupRemoved, flatten(assoc.Removed))
if writeErr != nil {
return writeErr
}
if err := w.Flush(); err != nil { if err := w.Flush(); err != nil {
return err return err
} }
+1 -1
View File
@@ -132,7 +132,7 @@ func (c *GeoClueClient) startSignalPump() error {
if err := c.dbusConn.AddMatchSignal( if err := c.dbusConn.AddMatchSignal(
dbus.WithMatchObjectPath(c.clientPath), dbus.WithMatchObjectPath(c.clientPath),
dbus.WithMatchInterface(dbusGeoClueClientInterface), dbus.WithMatchInterface(dbusGeoClueClientInterface),
dbus.WithMatchSender(dbusGeoClueClientLocationUpdated), dbus.WithMatchMember("LocationUpdated"),
); err != nil { ); err != nil {
return err return err
} }
+14 -2
View File
@@ -529,11 +529,23 @@ func execFromDesktopFile(path string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
inDesktopEntry := false
for line := range strings.SplitSeq(string(data), "\n") { for line := range strings.SplitSeq(string(data), "\n") {
trimmed := strings.TrimSpace(line) trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "Exec=") { switch {
return strings.TrimSpace(trimmed[len("Exec="):]), nil case trimmed == "" || strings.HasPrefix(trimmed, "#"):
continue
case strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]"):
inDesktopEntry = trimmed == "[Desktop Entry]"
continue
case !inDesktopEntry:
continue
} }
key, value, found := strings.Cut(trimmed, "=")
if !found || strings.TrimSpace(key) != "Exec" {
continue
}
return strings.TrimSpace(value), nil
} }
return "", fmt.Errorf("no Exec= line found in %s", path) return "", fmt.Errorf("no Exec= line found in %s", path)
} }
+81 -3
View File
@@ -3,6 +3,7 @@ package greeter
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"syscall" "syscall"
@@ -93,18 +94,95 @@ func resolveSessionExecInDirs(sessionID string, dirs []string) (string, error) {
return "", fmt.Errorf("session desktop file %q was not found", id) return "", fmt.Errorf("session desktop file %q was not found", id)
} }
// parseExecString splits a Desktop Entry Exec= value into argv without
// involving a shell, mirroring quickshell's DesktopEntry::parseExecString
// (string quoting, value escapes, field code stripping).
func parseExecString(execLine string) []string {
var args []string
var cur strings.Builder
inString := false
escape := 0
percent := false
for _, c := range execLine {
switch {
case escape == 0 && c == '\\':
escape = 1
case inString:
switch {
case c == '\\':
escape++
if escape == 4 {
cur.WriteByte('\\')
escape = 0
}
case escape == 2:
cur.WriteRune(c)
escape = 0
case escape != 0:
switch c {
case 's':
cur.WriteByte(' ')
case 'n':
cur.WriteByte('\n')
case 't':
cur.WriteByte('\t')
case 'r':
cur.WriteByte('\r')
default:
cur.WriteRune(c)
}
escape = 0
case c == '"' || c == '\'':
inString = false
default:
cur.WriteRune(c)
}
case escape != 0:
cur.WriteRune(c)
escape = 0
case percent:
if c == '%' {
cur.WriteByte('%')
}
percent = false
case c == '%':
percent = true
case c == '"' || c == '\'':
inString = true
case c == ' ':
if cur.Len() > 0 {
args = append(args, cur.String())
cur.Reset()
}
default:
cur.WriteRune(c)
}
}
if cur.Len() > 0 {
args = append(args, cur.String())
}
return args
}
func LaunchSessionByID(sessionID string) error { func LaunchSessionByID(sessionID string) error {
execLine, err := ResolveSessionExec(sessionID) execLine, err := ResolveSessionExec(sessionID)
if err != nil { if err != nil {
return err return err
} }
execLine = strings.TrimSpace(stripDesktopExecCodes(execLine))
if execLine == "" { argv := parseExecString(strings.TrimSpace(execLine))
if len(argv) == 0 {
return fmt.Errorf("session %q has an empty Exec command", sessionID) return fmt.Errorf("session %q has an empty Exec command", sessionID)
} }
resolved, err := exec.LookPath(argv[0])
if err != nil {
return fmt.Errorf("session %q command %q not found: %w", sessionID, argv[0], err)
}
env := append(os.Environ(), "XDG_SESSION_TYPE=wayland") env := append(os.Environ(), "XDG_SESSION_TYPE=wayland")
return syscall.Exec("/bin/sh", []string{"sh", "-c", "exec " + execLine}, env) return syscall.Exec(resolved, argv, env)
} }
func LaunchSessionFromMemory(cacheDir, homeDir string) error { func LaunchSessionFromMemory(cacheDir, homeDir string) error {
@@ -0,0 +1,57 @@
package greeter
import (
"path/filepath"
"reflect"
"testing"
)
func TestParseExecString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
exec string
want []string
}{
{"plain", "niri --session", []string{"niri", "--session"}},
{"extra spaces", "niri --session", []string{"niri", "--session"}},
{"double quoted arg", `env "with space" run`, []string{"env", "with space", "run"}},
{"single quoted arg", `env 'with space' run`, []string{"env", "with space", "run"}},
{"escaped quote in quotes", `sh "say \\"hi\\""`, []string{"sh", `say "hi"`}},
{"field code dropped", "gnome-session %U", []string{"gnome-session"}},
{"field code mid-arg", "app --url=%u --run", []string{"app", "--url=", "--run"}},
{"literal percent", "app 100%% done", []string{"app", "100%", "done"}},
{"shell metachars stay literal", "sh -c $(reboot); echo", []string{"sh", "-c", "$(reboot);", "echo"}},
{"empty", "", nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := parseExecString(tt.exec); !reflect.DeepEqual(got, tt.want) {
t.Fatalf("parseExecString(%q) = %#v, want %#v", tt.exec, got, tt.want)
}
})
}
}
func TestExecFromDesktopFileOnlyReadsDesktopEntryGroup(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "example.desktop")
writeTestFile(t, path, `[Desktop Action other]
Exec=/wrong/binary
[Desktop Entry]
Name=Example
Exec = /right/binary --flag
`)
got, err := execFromDesktopFile(path)
if err != nil {
t.Fatalf("execFromDesktopFile returned error: %v", err)
}
if got != "/right/binary --flag" {
t.Fatalf("execFromDesktopFile = %q, want %q", got, "/right/binary --flag")
}
}
+33 -16
View File
@@ -299,6 +299,9 @@ type hyprlandOverrideBind struct {
Options map[string]any Options map[string]any
// Unbind: negative override (hl.unbind only, no rebind). // Unbind: negative override (hl.unbind only, no rebind).
Unbind bool Unbind bool
// RawLuaAction: Action is a custom hl.* Lua expression round-tripped from an
// existing Lua override; re-emit it verbatim instead of quoting it.
RawLuaAction bool
} }
func (h *HyprlandProvider) ensureWritableConfig() error { func (h *HyprlandProvider) ensureWritableConfig() error {
@@ -1046,18 +1049,26 @@ func luaActionStringFromHyprlangAction(action string) string {
if expr, ok := luaActionStringFromKnownHyprlandAction(action); ok { if expr, ok := luaActionStringFromKnownHyprlandAction(action); ok {
return expr return expr
} }
return action // Unrecognized dispatchers are freeform text, not Lua; forward them to
// hyprctl quoted so a stray `"` can't produce broken Lua output.
return luaHyprctlDispatchFunction(action)
} }
func luaExprToInternalAction(expr string) string { // luaExprToInternalAction converts a parsed Lua bind expression back into
// "dispatcher params" text. isRawLua reports that expr matched no known hl.*
// shape and must be re-emitted verbatim as Lua on write-back.
func luaExprToInternalAction(expr string) (action string, isRawLua bool) {
d, p := luaExprToDispatcherParams(expr) d, p := luaExprToDispatcherParams(expr)
if d == expr && p == "" {
return expr, true
}
if d == "exec" && p != "" && !strings.HasPrefix(p, "hyprctl dispatch lua:") { if d == "exec" && p != "" && !strings.HasPrefix(p, "hyprctl dispatch lua:") {
return "exec " + p return "exec " + p, false
} }
if p != "" { if p != "" {
return d + " " + p return d + " " + p, false
} }
return d return d, false
} }
func luaBindOptions(bind *hyprlandOverrideBind) []string { func luaBindOptions(bind *hyprlandOverrideBind) []string {
@@ -1075,20 +1086,25 @@ func luaBindOptions(bind *hyprlandOverrideBind) []string {
} }
func writeLuaBindLine(sb *strings.Builder, bind *hyprlandOverrideBind) { func writeLuaBindLine(sb *strings.Builder, bind *hyprlandOverrideBind) {
key := formatLuaBindKey(bind.Key) key := strconv.Quote(formatLuaBindKey(bind.Key))
if bind.Unbind { if bind.Unbind {
fmt.Fprintf(sb, `hl.unbind("%s")`, key) fmt.Fprintf(sb, `hl.unbind(%s)`, key)
sb.WriteByte('\n') sb.WriteByte('\n')
return return
} }
expr := luaActionStringFromHyprlangAction(bind.Action) var expr string
if bind.RawLuaAction {
expr = bind.Action
} else {
expr = luaActionStringFromHyprlangAction(bind.Action)
}
opts := luaBindOptions(bind) opts := luaBindOptions(bind)
fmt.Fprintf(sb, `hl.unbind("%s")`, key) fmt.Fprintf(sb, `hl.unbind(%s)`, key)
sb.WriteByte('\n') sb.WriteByte('\n')
if len(opts) > 0 { if len(opts) > 0 {
fmt.Fprintf(sb, `hl.bind("%s", %s, { %s })`, key, expr, strings.Join(opts, ", ")) fmt.Fprintf(sb, `hl.bind(%s, %s, { %s })`, key, expr, strings.Join(opts, ", "))
} else { } else {
fmt.Fprintf(sb, `hl.bind("%s", %s)`, key, expr) fmt.Fprintf(sb, `hl.bind(%s, %s)`, key, expr)
} }
sb.WriteByte('\n') sb.WriteByte('\n')
} }
@@ -1104,17 +1120,18 @@ func parseLuaBindOverrideLine(line string) (*hyprlandOverrideBind, bool) {
} }
internalKey := luaKeyComboToInternalKey(kbc) internalKey := luaKeyComboToInternalKey(kbc)
action := luaExprToInternalAction(actionExpr) action, isRawLua := luaExprToInternalAction(actionExpr)
flags := luaBindOptFlags(optSuffix) flags := luaBindOptFlags(optSuffix)
description := luaBindOptDescription(optSuffix) description := luaBindOptDescription(optSuffix)
if description == "" { if description == "" {
description = luaLineTrailingComment(line) description = luaLineTrailingComment(line)
} }
return &hyprlandOverrideBind{ return &hyprlandOverrideBind{
Key: internalKey, Key: internalKey,
Action: action, Action: action,
Description: description, Description: description,
Flags: flags, Flags: flags,
RawLuaAction: isRawLua,
}, true }, true
} }
@@ -147,9 +147,10 @@ hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"), { descripti
func TestWriteLuaBindLineLeavesCustomLuaDispatcherRaw(t *testing.T) { func TestWriteLuaBindLineLeavesCustomLuaDispatcherRaw(t *testing.T) {
var sb strings.Builder var sb strings.Builder
writeLuaBindLine(&sb, &hyprlandOverrideBind{ writeLuaBindLine(&sb, &hyprlandOverrideBind{
Key: "Super+u", Key: "Super+u",
Action: "hl.dsp.no_op()", Action: "hl.dsp.no_op()",
Description: "Custom Lua", Description: "Custom Lua",
RawLuaAction: true,
}) })
want := `hl.unbind("SUPER + U") want := `hl.unbind("SUPER + U")
@@ -159,6 +160,24 @@ hl.bind("SUPER + U", hl.dsp.no_op(), { description = "Custom Lua" })`
} }
} }
func TestWriteLuaBindLineQuotesUnrecognizedActionWithoutRawLuaFlag(t *testing.T) {
var sb strings.Builder
writeLuaBindLine(&sb, &hyprlandOverrideBind{
Key: "Super+u",
Action: `customdispatcher "),os.execute("id")--`,
})
got := sb.String()
if !strings.Contains(got, "hl.exec_cmd(") {
t.Fatalf("expected unrecognized action to go through the hyprctl-dispatch wrapper, got %q", got)
}
// an unpaired bare quote means the action broke out of its string literal
withoutEscapedQuotes := strings.ReplaceAll(got, `\"`, "")
if n := strings.Count(withoutEscapedQuotes, `"`); n%2 != 0 {
t.Fatalf("action broke out of its string literal (%d unpaired quotes): %q", n, got)
}
}
func TestLuaActionStringFromHyprlangActionUsesNativeDispatchers(t *testing.T) { func TestLuaActionStringFromHyprlangActionUsesNativeDispatchers(t *testing.T) {
tests := []struct { tests := []struct {
action string action string
@@ -226,15 +245,12 @@ func TestParseLuaBindLineHandlesFunctionDispatcherFallback(t *testing.T) {
} }
} }
func TestLuaActionStringLeavesCustomLuaDispatcherRaw(t *testing.T) { func TestLuaActionStringFromHyprlangActionAlwaysQuotesUnrecognizedText(t *testing.T) {
got := luaActionStringFromHyprlangAction("hl.dsp.no_op()") got := luaActionStringFromHyprlangAction("hl.dsp.no_op()")
want := `hl.dsp.no_op()` want := `function() hl.exec_cmd("hyprctl dispatch hl.dsp.no_op()") end`
if got != want { if got != want {
t.Fatalf("luaActionStringFromHyprlangAction() = %q, want %q", got, want) t.Fatalf("luaActionStringFromHyprlangAction() = %q, want %q", got, want)
} }
if strings.Contains(got, "hl.dispatch") || strings.Contains(got, "hyprctl dispatch") {
t.Fatalf("expected custom Lua dispatcher expression to stay raw, got %q", got)
}
} }
func TestReadLuaOverrideMigratesTrailingCommentToDescription(t *testing.T) { func TestReadLuaOverrideMigratesTrailingCommentToDescription(t *testing.T) {
+17
View File
@@ -64,7 +64,20 @@ func (m *Manager) findInstalledPath(pluginID string) (string, error) {
return m.findInDir(systemDir, pluginID) return m.findInDir(systemDir, pluginID)
} }
// isSafePluginPathComponent rejects ids that aren't a single path component,
// so filepath.Join can't resolve (and later RemoveAll) outside the plugins dir
func isSafePluginPathComponent(s string) bool {
if s == "" || s == "." || s == ".." {
return false
}
return !strings.ContainsAny(s, "/\\")
}
func (m *Manager) findInDir(dir, pluginID string) (string, error) { func (m *Manager) findInDir(dir, pluginID string) (string, error) {
if !isSafePluginPathComponent(pluginID) {
return "", fmt.Errorf("invalid plugin id: %q", pluginID)
}
// First, check if folder with exact ID name exists // First, check if folder with exact ID name exists
exactPath := filepath.Join(dir, pluginID) exactPath := filepath.Join(dir, pluginID)
if exists, _ := afero.DirExists(m.fs, exactPath); exists { if exists, _ := afero.DirExists(m.fs, exactPath); exists {
@@ -507,6 +520,10 @@ func (m *Manager) findInstalledPathByIDOrName(idOrName string) (string, error) {
} }
func (m *Manager) findInDirByIDOrName(dir, idOrName string) (string, error) { func (m *Manager) findInDirByIDOrName(dir, idOrName string) (string, error) {
if !isSafePluginPathComponent(idOrName) {
return "", fmt.Errorf("invalid plugin id/name: %q", idOrName)
}
// Check exact folder name match first // Check exact folder name match first
exactPath := filepath.Join(dir, idOrName) exactPath := filepath.Join(dir, idOrName)
if exists, _ := afero.DirExists(m.fs, exactPath); exists { if exists, _ := afero.DirExists(m.fs, exactPath); exists {
+16 -10
View File
@@ -135,15 +135,14 @@ func EscapeSingleQuotes(s string) string {
} }
// MakeCommand returns a bash command string that runs `command` with the // MakeCommand returns a bash command string that runs `command` with the
// detected tool. When the tool supports stdin passwords and password is // detected tool, prompting interactively on a TTY where applicable. The
// non-empty, the password is piped in. Otherwise the tool is invoked with // sudo-with-password case lives in ExecCommand, which pipes the password via
// no non-interactive flag so that an interactive TTY prompt is still // stdin so it never lands in argv.
// possible for CLI callers.
// //
// If detection fails, the returned shell string exits 1 with an error // If detection fails, the returned shell string exits 1 with an error
// message so callers that treat the *exec.Cmd as infallible still fail // message so callers that treat the *exec.Cmd as infallible still fail
// deterministically. // deterministically.
func MakeCommand(password, command string) string { func MakeCommand(command string) string {
t, err := Detect() t, err := Detect()
if err != nil { if err != nil {
return failingShell(err) return failingShell(err)
@@ -151,9 +150,6 @@ func MakeCommand(password, command string) string {
switch t { switch t {
case ToolSudo: case ToolSudo:
if password != "" {
return fmt.Sprintf("echo '%s' | sudo -S %s", EscapeSingleQuotes(password), command)
}
return fmt.Sprintf("sudo %s", command) return fmt.Sprintf("sudo %s", command)
case ToolDoas: case ToolDoas:
return fmt.Sprintf("doas sh -c '%s'", EscapeSingleQuotes(command)) return fmt.Sprintf("doas sh -c '%s'", EscapeSingleQuotes(command))
@@ -166,9 +162,19 @@ func MakeCommand(password, command string) string {
// ExecCommand builds an exec.Cmd that runs `command` as root via the // ExecCommand builds an exec.Cmd that runs `command` as root via the
// detected tool. Detection errors surface at Run() time as a failing // detected tool. Detection errors surface at Run() time as a failing
// command writing a clear error to stderr. // command writing a clear error to stderr. A sudo password is piped via
// stdin (sudo -S) so it never appears in argv.
func ExecCommand(ctx context.Context, password, command string) *exec.Cmd { func ExecCommand(ctx context.Context, password, command string) *exec.Cmd {
return exec.CommandContext(ctx, "bash", "-c", MakeCommand(password, command)) t, err := Detect()
if err != nil {
return exec.CommandContext(ctx, "bash", "-c", failingShell(err))
}
if t == ToolSudo && password != "" {
cmd := exec.CommandContext(ctx, "sudo", "-S", "sh", "-c", command)
cmd.Stdin = strings.NewReader(password + "\n")
return cmd
}
return exec.CommandContext(ctx, "bash", "-c", MakeCommand(command))
} }
// ExecArgv builds an exec.Cmd that runs argv as root via the detected tool. // ExecArgv builds an exec.Cmd that runs argv as root via the detected tool.
+6
View File
@@ -747,12 +747,16 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
bpp := format.BytesPerPixel() bpp := format.BytesPerPixel()
if int(e.Stride) < int(e.Width)*bpp { if int(e.Stride) < int(e.Width)*bpp {
log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width, "bpp", bpp) log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width, "bpp", bpp)
// bail out here or the dispatch loop waits forever on a ready/failed
// event that never comes (frame.Copy is never called)
failed = true
return return
} }
var err error var err error
buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride)) buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
if err != nil { if err != nil {
log.Error("failed to create buffer", "err", err) log.Error("failed to create buffer", "err", err)
failed = true
return return
} }
buf.Format = format buf.Format = format
@@ -771,6 +775,7 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
pool, err = s.shm.CreatePool(buf.Fd(), int32(buf.Size())) pool, err = s.shm.CreatePool(buf.Fd(), int32(buf.Size()))
if err != nil { if err != nil {
log.Error("failed to create pool", "err", err) log.Error("failed to create pool", "err", err)
failed = true
return return
} }
@@ -779,6 +784,7 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
pool.Destroy() pool.Destroy()
pool = nil pool = nil
log.Error("failed to create wl_buffer", "err", err) log.Error("failed to create wl_buffer", "err", err)
failed = true
return return
} }
+24 -11
View File
@@ -1839,21 +1839,34 @@ func (m *Manager) EntryToFile(entry *Entry) string {
return "" return ""
} }
func (m *Manager) dbusConnForFlatpak() (*dbus.Conn, error) {
m.dbusConnMutex.Lock()
defer m.dbusConnMutex.Unlock()
if m.dbusConn != nil {
return m.dbusConn, nil
}
conn, err := dbus.ConnectSessionBus()
if err != nil {
return nil, fmt.Errorf("connect session bus: %w", err)
}
if !conn.SupportsUnixFDs() {
conn.Close()
return nil, fmt.Errorf("D-Bus connection does not support Unix FD passing")
}
m.dbusConn = conn
return conn, nil
}
func (m *Manager) ExportFileForFlatpak(filePath string) (string, error) { func (m *Manager) ExportFileForFlatpak(filePath string) (string, error) {
if _, err := os.Stat(filePath); err != nil { if _, err := os.Stat(filePath); err != nil {
return "", fmt.Errorf("file not found: %w", err) return "", fmt.Errorf("file not found: %w", err)
} }
if m.dbusConn == nil { dbusConn, err := m.dbusConnForFlatpak()
conn, err := dbus.ConnectSessionBus() if err != nil {
if err != nil { return "", err
return "", fmt.Errorf("connect session bus: %w", err)
}
if !conn.SupportsUnixFDs() {
conn.Close()
return "", fmt.Errorf("D-Bus connection does not support Unix FD passing")
}
m.dbusConn = conn
} }
file, err := os.Open(filePath) file, err := os.Open(filePath)
@@ -1862,7 +1875,7 @@ func (m *Manager) ExportFileForFlatpak(filePath string) (string, error) {
} }
fd := int(file.Fd()) fd := int(file.Fd())
portal := m.dbusConn.Object("org.freedesktop.portal.Documents", "/org/freedesktop/portal/documents") portal := dbusConn.Object("org.freedesktop.portal.Documents", "/org/freedesktop/portal/documents")
var docIds []string var docIds []string
var extra map[string]dbus.Variant var extra map[string]dbus.Variant
+3 -1
View File
@@ -153,7 +153,9 @@ type Manager struct {
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastState *State lastState *State
dbusConn *dbus.Conn // lazily created by dbusConnForFlatpak under dbusConnMutex
dbusConn *dbus.Conn
dbusConnMutex sync.Mutex
} }
func (m *Manager) GetState() State { func (m *Manager) GetState() State {
+12
View File
@@ -37,6 +37,9 @@ func (sm *SubscriptionManager) Start() error {
return fmt.Errorf("subscription manager already running") return fmt.Errorf("subscription manager already running")
} }
sm.running = true sm.running = true
// replace the channel closed by the previous Stop(); doing it here rather
// than in Stop() guarantees a lagging eventHandler still observes the close
sm.eventChan = make(chan SubscriptionEvent, 100)
sm.mu.Unlock() sm.mu.Unlock()
subID, err := sm.createSubscription() subID, err := sm.createSubscription()
@@ -206,6 +209,8 @@ func (sm *SubscriptionManager) parseEvent(attrs ipp.Attributes) SubscriptionEven
} }
func (sm *SubscriptionManager) Events() <-chan SubscriptionEvent { func (sm *SubscriptionManager) Events() <-chan SubscriptionEvent {
sm.mu.Lock()
defer sm.mu.Unlock()
return sm.eventChan return sm.eventChan
} }
@@ -228,6 +233,13 @@ func (sm *SubscriptionManager) Stop() {
} }
sm.stopChan = make(chan struct{}) sm.stopChan = make(chan struct{})
// the writer (notificationLoop) joined above, so closing is safe; without
// this close Manager.eventHandler never returns and Unsubscribe deadlocks
// on eventWG.Wait(). Start() allocates the replacement.
sm.mu.Lock()
close(sm.eventChan)
sm.mu.Unlock()
} }
func (sm *SubscriptionManager) cancelSubscription() { func (sm *SubscriptionManager) cancelSubscription() {
@@ -38,6 +38,8 @@ func (sm *DBusSubscriptionManager) Start() error {
return fmt.Errorf("subscription manager already running") return fmt.Errorf("subscription manager already running")
} }
sm.running = true sm.running = true
// replaced here rather than in Stop(); see SubscriptionManager.Start()
sm.eventChan = make(chan SubscriptionEvent, 100)
sm.mu.Unlock() sm.mu.Unlock()
conn, err := dbus.ConnectSystemBus() conn, err := dbus.ConnectSystemBus()
@@ -252,6 +254,8 @@ func (sm *DBusSubscriptionManager) parseDBusSignal(sig *dbus.Signal) Subscriptio
} }
func (sm *DBusSubscriptionManager) Events() <-chan SubscriptionEvent { func (sm *DBusSubscriptionManager) Events() <-chan SubscriptionEvent {
sm.mu.Lock()
defer sm.mu.Unlock()
return sm.eventChan return sm.eventChan
} }
@@ -278,6 +282,12 @@ func (sm *DBusSubscriptionManager) Stop() {
} }
sm.stopChan = make(chan struct{}) sm.stopChan = make(chan struct{})
// the writer (dbusListenerLoop) joined above, so closing is safe; see
// SubscriptionManager.Stop()
sm.mu.Lock()
close(sm.eventChan)
sm.mu.Unlock()
} }
func (sm *DBusSubscriptionManager) cancelSubscription() { func (sm *DBusSubscriptionManager) cancelSubscription() {
+4
View File
@@ -201,6 +201,10 @@ func handleListNames(conn net.Conn, req models.Request, m *Manager) {
} }
func handleSubscribe(conn net.Conn, req models.Request, m *Manager, clientID string) { func handleSubscribe(conn net.Conn, req models.Request, m *Manager, clientID string) {
if id := params.StringOpt(req.Params, "clientId", ""); id != "" {
clientID = id
}
bus, err := params.String(req.Params, "bus") bus, err := params.String(req.Params, "bus")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
+19 -4
View File
@@ -137,22 +137,25 @@ func (m *Manager) consumeSelfEcho(value uint32) bool {
} }
func (m *Manager) watchSettingsChanges() { func (m *Manager) watchSettingsChanges() {
conn, err := dbus.ConnectSessionBus() // reuse the shared session connection; a dedicated one was unreachable
if err != nil { // from Close() and leaked with this goroutine
log.Warnf("color-scheme watcher: session bus connect: %v", err) if m.sessionConn == nil {
return return
} }
conn := m.sessionConn
if err := conn.AddMatchSignal( if err := conn.AddMatchSignal(
dbus.WithMatchInterface(dbusPortalSettingsInterface), dbus.WithMatchInterface(dbusPortalSettingsInterface),
dbus.WithMatchMember("SettingChanged"), dbus.WithMatchMember("SettingChanged"),
); err != nil { ); err != nil {
log.Warnf("Failed to watch portal settings changes: %v", err) log.Warnf("Failed to watch portal settings changes: %v", err)
conn.Close()
return return
} }
signals := make(chan *dbus.Signal, 64) signals := make(chan *dbus.Signal, 64)
m.stateMutex.Lock()
m.settingsSignals = signals
m.stateMutex.Unlock()
conn.Signal(signals) conn.Signal(signals)
for sig := range signals { for sig := range signals {
@@ -309,6 +312,18 @@ func (m *Manager) Close() {
m.systemConn.Close() m.systemConn.Close()
} }
if m.sessionConn != nil { if m.sessionConn != nil {
m.sessionConn.RemoveMatchSignal(
dbus.WithMatchInterface(dbusPortalSettingsInterface),
dbus.WithMatchMember("SettingChanged"),
)
m.stateMutex.Lock()
signals := m.settingsSignals
m.settingsSignals = nil
m.stateMutex.Unlock()
if signals != nil {
m.sessionConn.RemoveSignal(signals)
close(signals)
}
m.sessionConn.Close() m.sessionConn.Close()
} }
} }
@@ -71,4 +71,6 @@ type Manager struct {
screensaverGnomeClaimed bool screensaverGnomeClaimed bool
selfEchoMu sync.Mutex selfEchoMu sync.Mutex
selfEchoes []colorSchemeEcho selfEchoes []colorSchemeEcho
// registered on sessionConn by watchSettingsChanges; guarded by stateMutex
settingsSignals chan *dbus.Signal
} }
@@ -2,6 +2,7 @@ package network
import ( import (
"fmt" "fmt"
"maps"
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -57,6 +58,11 @@ type NetworkManagerBackend struct {
wifiDev any wifiDev any
wifiDevices map[string]*wifiDeviceInfo wifiDevices map[string]*wifiDeviceInfo
// devMutex guards ethernetDevices/wifiDevices (written by the signal pump,
// read by request handlers). Not reentrant — never hold it across calls
// into other backend methods.
devMutex sync.RWMutex
dbusConn *dbus.Conn dbusConn *dbus.Conn
signals chan *dbus.Signal signals chan *dbus.Signal
sigWG sync.WaitGroup sigWG sync.WaitGroup
@@ -185,12 +191,12 @@ func (b *NetworkManagerBackend) Initialize() error {
} }
hwAddr, _ := w.GetPropertyHwAddress() hwAddr, _ := w.GetPropertyHwAddress()
b.ethernetDevices[iface] = &ethernetDeviceInfo{ b.setEthernetDeviceInfo(iface, &ethernetDeviceInfo{
device: dev, device: dev,
wired: w, wired: w,
name: iface, name: iface,
hwAddress: hwAddr, hwAddress: hwAddr,
} })
if b.ethernetDevice == nil { if b.ethernetDevice == nil {
b.ethernetDevice = dev b.ethernetDevice = dev
@@ -214,12 +220,12 @@ func (b *NetworkManagerBackend) Initialize() error {
} }
hwAddr, _ := w.GetPropertyHwAddress() hwAddr, _ := w.GetPropertyHwAddress()
b.wifiDevices[iface] = &wifiDeviceInfo{ b.setWifiDeviceInfo(iface, &wifiDeviceInfo{
device: dev, device: dev,
wireless: w, wireless: w,
name: iface, name: iface,
hwAddress: hwAddr, hwAddress: hwAddr,
} })
if b.wifiDevice == nil { if b.wifiDevice == nil {
b.wifiDevice = dev b.wifiDevice = dev
@@ -267,6 +273,80 @@ func (b *NetworkManagerBackend) Initialize() error {
return nil return nil
} }
func (b *NetworkManagerBackend) ethernetDevicesSnapshot() map[string]*ethernetDeviceInfo {
b.devMutex.RLock()
defer b.devMutex.RUnlock()
out := make(map[string]*ethernetDeviceInfo, len(b.ethernetDevices))
maps.Copy(out, b.ethernetDevices)
return out
}
func (b *NetworkManagerBackend) wifiDevicesSnapshot() map[string]*wifiDeviceInfo {
b.devMutex.RLock()
defer b.devMutex.RUnlock()
out := make(map[string]*wifiDeviceInfo, len(b.wifiDevices))
maps.Copy(out, b.wifiDevices)
return out
}
func (b *NetworkManagerBackend) ethernetDeviceByIface(iface string) (*ethernetDeviceInfo, bool) {
b.devMutex.RLock()
defer b.devMutex.RUnlock()
info, ok := b.ethernetDevices[iface]
return info, ok
}
func (b *NetworkManagerBackend) wifiDeviceByIface(iface string) (*wifiDeviceInfo, bool) {
b.devMutex.RLock()
defer b.devMutex.RUnlock()
info, ok := b.wifiDevices[iface]
return info, ok
}
func (b *NetworkManagerBackend) setEthernetDeviceInfo(iface string, info *ethernetDeviceInfo) {
b.devMutex.Lock()
b.ethernetDevices[iface] = info
b.devMutex.Unlock()
}
func (b *NetworkManagerBackend) setWifiDeviceInfo(iface string, info *wifiDeviceInfo) {
b.devMutex.Lock()
b.wifiDevices[iface] = info
b.devMutex.Unlock()
}
// removeEthernetDeviceByPath deletes the device and returns a snapshot of
// what's left so the caller can pick a replacement without holding devMutex
func (b *NetworkManagerBackend) removeEthernetDeviceByPath(path dbus.ObjectPath) (removed *ethernetDeviceInfo, remaining map[string]*ethernetDeviceInfo, found bool) {
b.devMutex.Lock()
defer b.devMutex.Unlock()
for iface, info := range b.ethernetDevices {
if info.device.GetPath() != path {
continue
}
delete(b.ethernetDevices, iface)
remaining = make(map[string]*ethernetDeviceInfo, len(b.ethernetDevices))
maps.Copy(remaining, b.ethernetDevices)
return info, remaining, true
}
return nil, nil, false
}
func (b *NetworkManagerBackend) removeWifiDeviceByPath(path dbus.ObjectPath) (removed *wifiDeviceInfo, remaining map[string]*wifiDeviceInfo, found bool) {
b.devMutex.Lock()
defer b.devMutex.Unlock()
for iface, info := range b.wifiDevices {
if info.device.GetPath() != path {
continue
}
delete(b.wifiDevices, iface)
remaining = make(map[string]*wifiDeviceInfo, len(b.wifiDevices))
maps.Copy(remaining, b.wifiDevices)
return info, remaining, true
}
return nil, nil, false
}
func (b *NetworkManagerBackend) Close() { func (b *NetworkManagerBackend) Close() {
close(b.stopChan) close(b.stopChan)
b.StopMonitoring() b.StopMonitoring()
@@ -323,7 +323,7 @@ func (b *NetworkManagerBackend) GetEthernetDevices() []EthernetDevice {
} }
func (b *NetworkManagerBackend) DisconnectEthernetDevice(device string) error { func (b *NetworkManagerBackend) DisconnectEthernetDevice(device string) error {
info, ok := b.ethernetDevices[device] info, ok := b.ethernetDeviceByIface(device)
if !ok { if !ok {
return fmt.Errorf("ethernet device %s not found", device) return fmt.Errorf("ethernet device %s not found", device)
} }
@@ -345,9 +345,10 @@ func (b *NetworkManagerBackend) DisconnectEthernetDevice(device string) error {
} }
func (b *NetworkManagerBackend) updateAllEthernetDevices() { func (b *NetworkManagerBackend) updateAllEthernetDevices() {
devices := make([]EthernetDevice, 0, len(b.ethernetDevices)) ethernetDevices := b.ethernetDevicesSnapshot()
devices := make([]EthernetDevice, 0, len(ethernetDevices))
for name, info := range b.ethernetDevices { for name, info := range ethernetDevices {
state, _ := info.device.GetPropertyState() state, _ := info.device.GetPropertyState()
connected := state == gonetworkmanager.NmDeviceStateActivated connected := state == gonetworkmanager.NmDeviceStateActivated
driver, _ := info.device.GetPropertyDriver() driver, _ := info.device.GetPropertyDriver()
@@ -112,7 +112,7 @@ func (b *NetworkManagerBackend) startSignalPump() error {
return err return err
} }
for _, info := range b.wifiDevices { for _, info := range b.wifiDevicesSnapshot() {
if err := conn.AddMatchSignal( if err := conn.AddMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())), dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())),
dbus.WithMatchInterface(dbusPropsInterface), dbus.WithMatchInterface(dbusPropsInterface),
@@ -124,7 +124,7 @@ func (b *NetworkManagerBackend) startSignalPump() error {
} }
} }
for _, info := range b.ethernetDevices { for _, info := range b.ethernetDevicesSnapshot() {
if err := conn.AddMatchSignal( if err := conn.AddMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())), dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())),
dbus.WithMatchInterface(dbusPropsInterface), dbus.WithMatchInterface(dbusPropsInterface),
@@ -227,7 +227,7 @@ func (b *NetworkManagerBackend) stopSignalPump() {
dbus.WithMatchMember("StateChanged"), dbus.WithMatchMember("StateChanged"),
) )
for _, info := range b.wifiDevices { for _, info := range b.wifiDevicesSnapshot() {
b.dbusConn.RemoveMatchSignal( b.dbusConn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())), dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())),
dbus.WithMatchInterface(dbusPropsInterface), dbus.WithMatchInterface(dbusPropsInterface),
@@ -235,7 +235,7 @@ func (b *NetworkManagerBackend) stopSignalPump() {
) )
} }
for _, info := range b.ethernetDevices { for _, info := range b.ethernetDevicesSnapshot() {
b.dbusConn.RemoveMatchSignal( b.dbusConn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())), dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())),
dbus.WithMatchInterface(dbusPropsInterface), dbus.WithMatchInterface(dbusPropsInterface),
@@ -550,12 +550,12 @@ func (b *NetworkManagerBackend) handleDeviceAdded(devicePath dbus.ObjectPath) {
} }
hwAddr, _ := w.GetPropertyHwAddress() hwAddr, _ := w.GetPropertyHwAddress()
b.ethernetDevices[iface] = &ethernetDeviceInfo{ b.setEthernetDeviceInfo(iface, &ethernetDeviceInfo{
device: dev, device: dev,
wired: w, wired: w,
name: iface, name: iface,
hwAddress: hwAddr, hwAddress: hwAddr,
} })
if b.ethernetDevice == nil { if b.ethernetDevice == nil {
b.ethernetDevice = dev b.ethernetDevice = dev
@@ -573,12 +573,12 @@ func (b *NetworkManagerBackend) handleDeviceAdded(devicePath dbus.ObjectPath) {
} }
hwAddr, _ := w.GetPropertyHwAddress() hwAddr, _ := w.GetPropertyHwAddress()
b.wifiDevices[iface] = &wifiDeviceInfo{ b.setWifiDeviceInfo(iface, &wifiDeviceInfo{
device: dev, device: dev,
wireless: w, wireless: w,
name: iface, name: iface,
hwAddress: hwAddr, hwAddress: hwAddr,
} })
if b.wifiDevice == nil { if b.wifiDevice == nil {
b.wifiDevice = dev b.wifiDevice = dev
@@ -603,57 +603,49 @@ func (b *NetworkManagerBackend) handleDeviceRemoved(devicePath dbus.ObjectPath)
) )
} }
for iface, info := range b.ethernetDevices { if _, remaining, found := b.removeEthernetDeviceByPath(devicePath); found {
if info.device.GetPath() == devicePath { if b.ethernetDevice != nil {
delete(b.ethernetDevices, iface) dev := b.ethernetDevice.(gonetworkmanager.Device)
if dev.GetPath() == devicePath {
if b.ethernetDevice != nil { b.ethernetDevice = nil
dev := b.ethernetDevice.(gonetworkmanager.Device) for _, r := range remaining {
if dev.GetPath() == devicePath { b.ethernetDevice = r.device
b.ethernetDevice = nil break
for _, remaining := range b.ethernetDevices {
b.ethernetDevice = remaining.device
break
}
} }
} }
b.updateAllEthernetDevices()
b.updateEthernetState()
b.listEthernetConnections()
b.updatePrimaryConnection()
if b.onStateChange != nil {
b.onStateChange()
}
return
} }
b.updateAllEthernetDevices()
b.updateEthernetState()
b.listEthernetConnections()
b.updatePrimaryConnection()
if b.onStateChange != nil {
b.onStateChange()
}
return
} }
for iface, info := range b.wifiDevices { if _, remaining, found := b.removeWifiDeviceByPath(devicePath); found {
if info.device.GetPath() == devicePath { if b.wifiDevice != nil {
delete(b.wifiDevices, iface) dev := b.wifiDevice.(gonetworkmanager.Device)
if dev.GetPath() == devicePath {
if b.wifiDevice != nil { b.wifiDevice = nil
dev := b.wifiDevice.(gonetworkmanager.Device) b.wifiDev = nil
if dev.GetPath() == devicePath { for _, r := range remaining {
b.wifiDevice = nil b.wifiDevice = r.device
b.wifiDev = nil b.wifiDev = r.wireless
for _, remaining := range b.wifiDevices { break
b.wifiDevice = remaining.device
b.wifiDev = remaining.wireless
break
}
} }
} }
b.updateAllWiFiDevices()
b.updateWiFiState()
if b.onStateChange != nil {
b.onStateChange()
}
return
} }
b.updateAllWiFiDevices()
b.updateWiFiState()
if b.onStateChange != nil {
b.onStateChange()
}
return
} }
} }
@@ -76,7 +76,7 @@ func (b *NetworkManagerBackend) updateEthernetState() error {
var connectedIP string var connectedIP string
var anyConnected bool var anyConnected bool
for name, info := range b.ethernetDevices { for name, info := range b.ethernetDevicesSnapshot() {
state, err := info.device.GetPropertyState() state, err := info.device.GetPropertyState()
if err != nil { if err != nil {
continue continue
@@ -973,7 +973,7 @@ func (b *NetworkManagerBackend) SetWiFiAutoconnect(ssid string, autoconnect bool
} }
func (b *NetworkManagerBackend) ScanWiFiDevice(device string) error { func (b *NetworkManagerBackend) ScanWiFiDevice(device string) error {
devInfo, ok := b.wifiDevices[device] devInfo, ok := b.wifiDeviceByIface(device)
if !ok { if !ok {
return fmt.Errorf("WiFi device not found: %s", device) return fmt.Errorf("WiFi device not found: %s", device)
} }
@@ -995,7 +995,7 @@ func (b *NetworkManagerBackend) ScanWiFiDevice(device string) error {
} }
func (b *NetworkManagerBackend) DisconnectWiFiDevice(device string) error { func (b *NetworkManagerBackend) DisconnectWiFiDevice(device string) error {
devInfo, ok := b.wifiDevices[device] devInfo, ok := b.wifiDeviceByIface(device)
if !ok { if !ok {
return fmt.Errorf("WiFi device not found: %s", device) return fmt.Errorf("WiFi device not found: %s", device)
} }
@@ -1047,7 +1047,7 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
wifiConnected := b.state.WiFiConnected wifiConnected := b.state.WiFiConnected
b.stateMutex.RUnlock() b.stateMutex.RUnlock()
for name, devInfo := range b.wifiDevices { for name, devInfo := range b.wifiDevicesSnapshot() {
state, _ := devInfo.device.GetPropertyState() state, _ := devInfo.device.GetPropertyState()
connected := state == gonetworkmanager.NmDeviceStateActivated connected := state == gonetworkmanager.NmDeviceStateActivated
@@ -1211,7 +1211,7 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
func (b *NetworkManagerBackend) getWifiDeviceForConnection(deviceName string) (*wifiDeviceInfo, error) { func (b *NetworkManagerBackend) getWifiDeviceForConnection(deviceName string) (*wifiDeviceInfo, error) {
if deviceName != "" { if deviceName != "" {
devInfo, ok := b.wifiDevices[deviceName] devInfo, ok := b.wifiDeviceByIface(deviceName)
if !ok { if !ok {
return nil, fmt.Errorf("WiFi device not found: %s", deviceName) return nil, fmt.Errorf("WiFi device not found: %s", deviceName)
} }
@@ -1224,7 +1224,7 @@ func (b *NetworkManagerBackend) getWifiDeviceForConnection(deviceName string) (*
dev := b.wifiDevice.(gonetworkmanager.Device) dev := b.wifiDevice.(gonetworkmanager.Device)
iface, _ := dev.GetPropertyInterface() iface, _ := dev.GetPropertyInterface()
if devInfo, ok := b.wifiDevices[iface]; ok { if devInfo, ok := b.wifiDeviceByIface(iface); ok {
return devInfo, nil return devInfo, nil
} }
+26 -4
View File
@@ -8,6 +8,7 @@ import (
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
"runtime/debug"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -41,7 +42,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
const APIVersion = 27 const APIVersion = 28
var CLIVersion = "dev" var CLIVersion = "dev"
@@ -398,6 +399,11 @@ func InitializeSysUpdateManager() error {
func handleConnection(conn net.Conn) { func handleConnection(conn net.Conn) {
defer conn.Close() defer conn.Close()
defer func() {
if r := recover(); r != nil {
log.Errorf("handleConnection panic recovered: panic=%v\n%s", r, debug.Stack())
}
}()
caps := getCapabilities() caps := getCapabilities()
capsData, _ := json.Marshal(caps) capsData, _ := json.Marshal(caps)
@@ -415,10 +421,21 @@ func handleConnection(conn net.Conn) {
continue continue
} }
go RouteRequest(conn, req) go routeRequestRecovered(conn, req)
} }
} }
// routeRequestRecovered keeps a panicking handler from taking down the whole daemon
func routeRequestRecovered(conn net.Conn, req models.Request) {
defer func() {
if r := recover(); r != nil {
log.Errorf("RouteRequest panic recovered: method=%s panic=%v\n%s", req.Method, r, debug.Stack())
models.RespondError(conn, req.ID, "internal server error")
}
}()
RouteRequest(conn, req)
}
func getCapabilities() Capabilities { func getCapabilities() Capabilities {
caps := []string{"plugins"} caps := []string{"plugins"}
@@ -581,6 +598,11 @@ func notifyCapabilityChange() {
func handleSubscribe(conn net.Conn, req models.Request) { func handleSubscribe(conn net.Conn, req models.Request) {
clientID := fmt.Sprintf("meta-client-%p", conn) clientID := fmt.Sprintf("meta-client-%p", conn)
dbusClient := dbusClientID
if id, ok := models.Get[string](req, "clientId"); ok && id != "" {
dbusClient = id
}
var services []string var services []string
if servicesParam, ok := models.Get[[]any](req, "services"); ok { if servicesParam, ok := models.Get[[]any](req, "services"); ok {
for _, s := range servicesParam { for _, s := range servicesParam {
@@ -1249,10 +1271,10 @@ func handleSubscribe(conn net.Conn, req models.Request) {
if shouldSubscribe("dbus") && dbusManager != nil { if shouldSubscribe("dbus") && dbusManager != nil {
wg.Add(1) wg.Add(1)
dbusChan := dbusManager.SubscribeSignals(dbusClientID) dbusChan := dbusManager.SubscribeSignals(dbusClient)
go func() { go func() {
defer wg.Done() defer wg.Done()
defer dbusManager.UnsubscribeSignals(dbusClientID) defer dbusManager.UnsubscribeSignals(dbusClient)
for { for {
select { select {
@@ -96,7 +96,7 @@ func (ctx *Context) GetDispatch() func() error {
} }
} }
return func() error { return func() (dispatchErr error) {
proxy, ok := ctx.objects.Load(senderID) proxy, ok := ctx.objects.Load(senderID)
if !ok { if !ok {
return nil // Proxy already deleted via delete_id, silently ignore return nil // Proxy already deleted via delete_id, silently ignore
@@ -111,6 +111,14 @@ func (ctx *Context) GetDispatch() func() error {
return fmt.Errorf("%w (senderID=%d)", ErrDispatchSenderUnsupported, senderID) return fmt.Errorf("%w (senderID=%d)", ErrDispatchSenderUnsupported, senderID)
} }
// generated Dispatch methods don't bounds-check wire data; surface a
// decoder panic as an error instead of crashing the process
defer func() {
if r := recover(); r != nil {
dispatchErr = fmt.Errorf("dispatch: panic handling opcode=%d senderID=%d: %v", opcode, senderID, r)
}
}()
sender.Dispatch(opcode, fd, data) sender.Dispatch(opcode, fd, data)
return nil return nil
} }
@@ -230,21 +230,6 @@ DankPopout {
elide: Text.ElideRight elide: Text.ElideRight
} }
StyledText {
id: hiddenRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: backendsRow.visible ? backendsRow.bottom : header.bottom
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingXS
visible: SystemUpdateService.hiddenUpdateCount > 0 && !SystemUpdateService.isUpgrading
text: I18n.tr("%1 hidden (AUR disabled or ignored)").arg(SystemUpdateService.hiddenUpdateCount)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Row { Row {
id: buttonsRow id: buttonsRow
anchors.left: parent.left anchors.left: parent.left
@@ -340,7 +325,7 @@ DankPopout {
id: bodyArea id: bodyArea
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.top: hiddenRow.visible ? hiddenRow.bottom : (backendsRow.visible ? backendsRow.bottom : header.bottom) anchors.top: backendsRow.visible ? backendsRow.bottom : header.bottom
anchors.bottom: buttonsRow.top anchors.bottom: buttonsRow.top
anchors.leftMargin: Theme.spacingL anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL anchors.rightMargin: Theme.spacingL
@@ -2101,6 +2101,14 @@ Item {
delegateRoot.updateAllData(); delegateRoot.updateAllData();
} }
} }
Connections {
target: CompositorService.isHyprland ? Hyprland : null
enabled: CompositorService.isHyprland
function onRawEvent(event) {
if (event.name === "activewindow" || event.name === "activewindowv2")
delegateRoot.updateAllData();
}
}
Connections { Connections {
target: I3.workspaces target: I3.workspaces
enabled: (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) enabled: (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
@@ -226,6 +226,13 @@ DankPopout {
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
MouseArea {
anchors.fill: parent
z: -1
enabled: root.__dropdownType !== 0
onClicked: root.__hideDropdowns()
}
implicitWidth: Math.max(700, pages.implicitWidth + (Theme.spacingM * 2)) implicitWidth: Math.max(700, pages.implicitWidth + (Theme.spacingM * 2))
implicitHeight: contentColumn.height + Theme.spacingM * 2 implicitHeight: contentColumn.height + Theme.spacingM * 2
color: "transparent" color: "transparent"
@@ -6,6 +6,7 @@ import qs.Widgets
Item { Item {
id: root id: root
visible: dropdownType !== 0
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
@@ -582,10 +583,4 @@ Item {
} }
} }
MouseArea {
anchors.fill: parent
z: -1
enabled: dropdownType !== 0
onClicked: closeRequested()
}
} }
+2 -3
View File
@@ -256,9 +256,8 @@ Singleton {
function getDevicePath(device) { function getDevicePath(device) {
if (!device || !device.address) { if (!device || !device.address) {
return ""; return "";
} }
const adapterPath = adapter ? "/org/bluez/hci0" : "/org/bluez/hci0"; return device.dbusPath ?? "";
return `${adapterPath}/dev_${device.address.replace(/:/g, "_")}`;
} }
function isAudioDevice(device) { function isAudioDevice(device) {
+8 -5
View File
@@ -246,13 +246,14 @@ Singleton {
function sendSubscribeRequest() { function sendSubscribeRequest() {
const request = { const request = {
"method": "subscribe" "method": "subscribe",
"params": {
"clientId": dbusClientId
}
}; };
if (activeSubscriptions.length > 0) { if (activeSubscriptions.length > 0) {
request.params = { request.params.services = activeSubscriptions;
"services": activeSubscriptions
};
log.debug("Subscribing to services:", JSON.stringify(activeSubscriptions)); log.debug("Subscribing to services:", JSON.stringify(activeSubscriptions));
} else { } else {
log.debug("Subscribing to all services"); log.debug("Subscribing to all services");
@@ -672,6 +673,7 @@ Singleton {
signal dbusSignalReceived(string subscriptionId, var data) signal dbusSignalReceived(string subscriptionId, var data)
readonly property string dbusClientId: "dms-qml-" + Date.now() + "-" + Math.floor(Math.random() * 0xffffffff)
property var dbusSubscriptions: ({}) property var dbusSubscriptions: ({})
function dbusCall(bus, dest, path, iface, method, args, callback) { function dbusCall(bus, dest, path, iface, method, args, callback) {
@@ -735,7 +737,8 @@ Singleton {
"sender": sender || "", "sender": sender || "",
"path": path || "", "path": path || "",
"interface": iface || "", "interface": iface || "",
"member": member || "" "member": member || "",
"clientId": dbusClientId
}, response => { }, response => {
if (!response.error && response.result?.subscriptionId) { if (!response.error && response.result?.subscriptionId) {
dbusSubscriptions[response.result.subscriptionId] = true; dbusSubscriptions[response.result.subscriptionId] = true;
@@ -33,7 +33,6 @@ Singleton {
property int nextCheckUnix: 0 property int nextCheckUnix: 0
readonly property int updateCount: availableUpdates.length readonly property int updateCount: availableUpdates.length
readonly property int hiddenUpdateCount: _rawUpdates.length - availableUpdates.length
readonly property bool helperAvailable: sysupdateAvailable && backends.length > 0 readonly property bool helperAvailable: sysupdateAvailable && backends.length > 0
Connections { Connections {
+46 -11
View File
@@ -996,18 +996,53 @@ Item {
height: root.renderedAlignedHeight + contentContainer.verticalConnectorExtent * 2 height: root.renderedAlignedHeight + contentContainer.verticalConnectorExtent * 2
} }
MouseArea { Item {
anchors.fill: parent id: backgroundClickCatcher
enabled: shouldBeVisible && backgroundInteractive
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
z: -1 z: -1
onClicked: mouse => { enabled: shouldBeVisible && backgroundInteractive
const clickX = mouse.x; x: 0
const clickY = mouse.y; y: 0
const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.renderedAlignedY || clickY > root.renderedAlignedY + root.renderedAlignedHeight; width: parent.width
if (!outsideContent) height: parent.height
return;
backgroundClicked(); // Four edge strips that exclude the popup body, so cursor shapes
// inside the content propagate correctly (full-screen MouseAreas
// at z:-1 can suppress child cursorShape on Wayland).
MouseArea {
x: 0
y: 0
width: parent.width
height: root.renderedAlignedY
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: backgroundClicked()
}
MouseArea {
x: 0
y: root.renderedAlignedY + root.renderedAlignedHeight
width: parent.width
height: Math.max(0, parent.height - root.renderedAlignedY - root.renderedAlignedHeight)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: backgroundClicked()
}
MouseArea {
x: 0
y: root.renderedAlignedY
width: root.alignedX
height: root.renderedAlignedHeight
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: backgroundClicked()
}
MouseArea {
x: root.alignedX + root.alignedWidth
y: root.renderedAlignedY
width: Math.max(0, parent.width - root.alignedX - root.alignedWidth)
height: root.renderedAlignedHeight
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: backgroundClicked()
} }
} }