spotifywm: No longer working with Spotify 1.1.55

After updating Spotify to latest 1.1.55.498.gf9a83c60, spotifywm is no longer working.

Output looks OK:

[spotifywm] attached to spotify
[spotifywm] attached to spotify
[spotifywm] attached to spotify

[spotifywm] attached to spotify
[spotifywm] spotify window found

But the window manager no longer applies any settings to the Spotify window on startup.

This same setup was working on previous versions of spotify-client, with nothing else changed.

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 19
  • Comments: 28 (1 by maintainers)

Most upvoted comments

I’m not too terribly familiar with how X works but I compiled a shared library very similiar to spotifywm that intercepts all of the windows creation and mapping functions I could find:

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif /* _GNU_SOURCE */

#include <X11/Xlib.h>
#include <X11/Xutil.h>

#include <assert.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

// Provided by glibc
extern char *program_invocation_short_name;

static void *dllXCreateWindow = NULL;
static void *dllXCreateSimpleWindow = NULL;
static void *dllXMapRaised = NULL;
static void *dllXMapSubwindows = NULL;
static void *dllXMapWindow = NULL;

static void SetClassHint(Display *display, Window w, char *name, char *cls)
{
    XClassHint *hint = XAllocClassHint();
    if (hint)
    {
        hint->res_name  = name;
        hint->res_class = cls;
        if (!XSetClassHint(display, w, hint))
        {
            fprintf(stderr, "[spotifywm] Failed to set class hint for window %ld\n", w);
        }

        XFree(hint);
    }
    else
    {
        fprintf(stderr, "[spotifywm] Failed to allocate class hints\n");
    }
}

Window XCreateWindow(Display *display, Window parent, int x, int y, unsigned int width, unsigned int height, unsigned int border_width, int depth, unsigned int class, Visual *visual, unsigned long valuemask, XSetWindowAttributes *attributes)
{
    Window w = ((int (*)(Display *, Window, int, int, unsigned int, unsigned int, unsigned int, int, unsigned int, Visual *, unsigned long, XSetWindowAttributes *)) dllXCreateWindow)(display, parent, x, y, width, height, border_width, depth, class, visual, valuemask, attributes);
    fprintf(stdout, "[spotifywm] 0x%lx = XCreateWindow(parent=0x%lx)\n", w, parent);
    return w;
}

Window XCreateSimpleWindow(Display *display, Window parent, int x, int y, unsigned int width, unsigned int height, unsigned int border_width, unsigned long border, unsigned long background)
{
    Window w = ((Window (*)(Display *, Window, int, int, unsigned int, unsigned int, unsigned int, unsigned long, unsigned long)) dllXCreateSimpleWindow)(display, parent, x, y, width, height, border_width, border, background);
    fprintf(stdout, "[spotifywm] 0x%lx = XCreateSimpleWindow(parent=0x%lx)\n", w, parent);
    return w;
}

int XMapRaised(Display *display, Window w)
{
    fprintf(stdout, "[spotifywm] XMapRaised(window=0x%lx)\n", w);
    return ((int (*)(Display *, Window)) dllXMapRaised)(display, w);
}

int XMapSubwindows(Display *display, Window w)
{
    fprintf(stdout, "[spotifywm] XMapSubwindows(window=0x%lx)\n", w);
    return ((int (*)(Display *, Window)) dllXMapSubwindows)(display, w);
}

int XMapWindow(Display *display, Window w)
{
    fprintf(stdout, "[spotifywm] XMapWindow(window=0x%lx)\n", w);

    SetClassHint(display, w, "spotify", "Spotify");
    return ((int (*)(Display *, Window)) dllXMapWindow)(display, w);
}

void spotifywm_init(void) __attribute__((constructor));
void spotifywm_init(void)
{
    if (strcmp(program_invocation_short_name, "spotify"))
    {
        fprintf(stderr, "[spotifywm] Program invocation didn't match 'spotify': %s\n",
                program_invocation_short_name);
    }

    dllXCreateWindow = dlsym(RTLD_NEXT, "XCreateWindow");
    dllXCreateSimpleWindow = dlsym(RTLD_NEXT, "XCreateSimpleWindow");
    dllXMapRaised = dlsym(RTLD_NEXT, "XMapRaised");
    dllXMapSubwindows = dlsym(RTLD_NEXT, "XMapSubwindows");
    dllXMapWindow = dlsym(RTLD_NEXT, "XMapWindow");

    assert(dllXCreateWindow);
    assert(dllXCreateSimpleWindow);
    assert(dllXMapRaised);
    assert(dllXMapSubwindows);
    assert(dllXMapWindow);
}

and is compiled with:

gcc -Wall -Wextra -O2 -shared -fPIC -static-libgcc -lX11 -o spotifywm.so spotifywm.c

Long story short, booting spotify outputs the following:

/opt/spotify/spotify: /usr/lib/libcurl-gnutls.so.4: no version information available (required by /opt/spotify/spotify)
/opt/spotify/spotify: /usr/lib/libcurl-gnutls.so.4: no version information available (required by /opt/spotify/spotify)
/opt/spotify/spotify: /usr/lib/libcurl-gnutls.so.4: no version information available (required by /opt/spotify/spotify)
[spotifywm] 0x6a00001 = XCreateWindow(parent=0x79b)
/proc/self/exe: /usr/lib/libcurl-gnutls.so.4: no version information available (required by /proc/self/exe)
[spotifywm] Program invocation didn't match 'spotify': exe
[spotifywm] 0x600000a = XCreateWindow(parent=0x5e00008)
[spotifywm] XMapWindow(window=0x600000a)
[spotifywm] 0x6000012 = XCreateWindow(parent=0x5e00008)
[spotifywm] XMapWindow(window=0x6000012)

showing the creation of three windows and the mapping of two. The issue seems to be though that the parent window for the latter two create calls is somehow owned by Spotify according to xlsw -r:

0x05E00007  ---  Spotify/spotify  Spotify Premium
  0x05E00008  --o  NA           NA
    0x06000012  ---  Spotify/spotify  NA

and doesn’t have it’s name or class set prior to being mapped (and we don’t see a mapping call being intercepted for that window ID). I’ve tried setting the class hint of the parent windows in the latter two mapping calls however it seems like that’s too late as my bspwm rule still doesn’t handle the spotify window properly.

Hoping someone more familiar with X may know how else the 0x5e00008 window in the above example could be getting created and/or mapped.

EDIT: Also potentially worth adding that xprop gives the Spotify window’s id as 0x5e00008 as well, not one the the windows we see being mapped above.

Same stuff on DWM

No!! don’t update!! give us the old version!!!

@mohad12211 You have to edit the rule matching code, which is inside the applyrules() function in dwm.c.

Same on XFWM4 with devilspie2. Devilspie2 sees both the window title and the app name as “Untitled window”, so here’s what I’m doing to work around it. The idea is that if both values are set to that and the spotify process is less than three seconds old, it’s reasonable to assume that the window belongs to Spotify. It could be reasonable to presume that Spotify is the only application on a given Linux desktop that would misbehave so flagrantly, but better safe than sorry.

if win_name == "Untitled window" and app_name == "Untitled window" then
    -- Let's find out if Spotify just started.
    handle = io.popen("pgrep -o spotify")
    proc = "/proc/"..handle:read()
    handle:close()
    handle = io.popen("stat -c%X "..proc)
    stat = handle:read()
    handle:close()
    if (os.time() - stat) < 3 then
        debug_print("Spotify just started; this window likely belongs to it.")
        set_class("Spotify")  -- Custom function to change the class for the rest of the scripts.
    end
end

Not at all an elegant solution, but I hacked a similar workaround for dwm:

	/* rule matching */
	c->isfloating = 0;
	c->tags = 0;
	XGetClassHint(dpy, c->win, &ch);
	if (0 == system("[ $(ps -o etimes= -p $(pgrep -o spotify)) -lt 3 ]")) {
		class    = ch.res_class ? ch.res_class : "Spotify";
	} else {
		class    = ch.res_class ? ch.res_class : broken;
	}
	instance = ch.res_name  ? ch.res_name  : broken;

In case I ever update it, here it is inside my build: https://github.com/BachoSeven/dwm/blob/master/dwm.c#L404

P.S. I found another window which has an initially broken class: Chromium’s Task Manager, the one brought up by Shift+Tab.

Same on XFWM4 with devilspie2. Devilspie2 sees both the window title and the app name as “Untitled window”, so here’s what I’m doing to work around it. The idea is that if both values are set to that and the spotify process is less than three seconds old, it’s reasonable to assume that the window belongs to Spotify. It could be reasonable to presume that Spotify is the only application on a given Linux desktop that would misbehave so flagrantly, but better safe than sorry.

if win_name == "Untitled window" and app_name == "Untitled window" then
    -- Let's find out if Spotify just started.
    handle = io.popen("pgrep -o spotify")
    proc = "/proc/"..handle:read()
    handle:close()
    handle = io.popen("stat -c%X "..proc)
    stat = handle:read()
    handle:close()
    if (os.time() - stat) < 3 then
        debug_print("Spotify just started; this window likely belongs to it.")
        set_class("Spotify")  -- Custom function to change the class for the rest of the scripts.
    end
end

Same on sway