Search code examples
haskellxmonad

How to restart a sandboxed and custom-compiled Xmonad


I wanted extend my xmonad.hs by moving it into its own sandboxed project environment via stack. It turns out that such sandboxing is possible via embedding your xmonad main function in a parent main that utilizes xmonad-entryhelper. Following the instructions in the projects README, converted xmonad.hs from this:

main :: IO ()
main = do
  statBar <- spawnPipe myXMobar
  xmonad def
    { terminal            = myTerminal
    , focusFollowsMouse   = myFocusFollowsMouse
    , borderWidth         = myBorderWidth
    , modMask             = myModMask
    , workspaces          = myWorkspaces
    , normalBorderColor   = myNormalBorderColor
    , focusedBorderColor  = myFocusedBorderColor

    -- key bindings
    , keys                = myKeys
    --, mouseBindings       = myMouseBindings

    -- hooks, layouts
    , layoutHook          = myLayoutHook
    , manageHook          = manageHook def <+> myManageHook
    --, handleEventHook     = myEventHook
    , logHook             = myLogHook statBar >> setWMName "LG3D"
    --, startupHook         = myStartupHook
    }

To...

kaleidoscope :: IO ()
kaleidoscope = do
  statBar <- spawnPipe myXMobar
  xmonad def
    { terminal            = myTerminal
    , focusFollowsMouse   = myFocusFollowsMouse
    , borderWidth         = myBorderWidth
    , modMask             = myModMask
    , workspaces          = myWorkspaces
    , normalBorderColor   = myNormalBorderColor
    , focusedBorderColor  = myFocusedBorderColor

    -- key bindings
    , keys                = myKeys
    --, mouseBindings       = myMouseBindings

    -- hooks, layouts
    , layoutHook          = myLayoutHook
    , manageHook          = manageHook def <+> myManageHook
    --, handleEventHook     = myEventHook
    , logHook             = myLogHook statBar >> setWMName "LG3D"
    --, startupHook         = myStartupHook
    }

main :: IO ()
main = EH.withCustomHelper kaleidoscopeConfig
  where
  kaleidoscopeConfig = EH.defaultConfig
    { EH.run = kaleidoscope
    , EH.compile = \force -> EH.withLock ExitSuccess $ do
        let cmd =
              if force
                then "cd /home/oldmanmike/src/github.com/oldmanmike/kaleidoscope && stack clean && stack build"
                else "cd /home/oldmanmike/src/github.com/oldmanmike/kaleidoscope && stack build"
        EH.compileUsingShell cmd
    , EH.postCompile = EH.defaultPostCompile
    }

So, I can now compile my project using xmonad --recompile and the binary found in .xmonad is soft linked to the binary produced by my sandboxed project.

But for some reason, xmonad --restart doesn't work anymore. What could be tripping it up?

The command itself doesn't seem to produce any errors from X - it just returns successfully with no sign anything even happened. I trying both the xmonad --restart command at the shell and binding mod-q in xmonad to either spawnPipe "xmonad --restart" or io sendRestart - neither seem to work. When I shut down my Xserver and restart it manually, any and all updates show up, but right now hot swapping changes doesn't seem to work. Does it need to see two separate binaries at once - the current one and new one?

EDIT: I've been playing around with the problem via reading the source code for xmonad, xmonad-entryhelper, and X11. I'm currently picking apart this:

sendRestart :: IO ()
sendRestart = do
    dpy <- openDisplay ""
    rw <- rootWindow dpy $ defaultScreen dpy
    xmonad_restart <- internAtom dpy "XMONAD_RESTART" False
    allocaXEvent $ \e -> do
        setEventType e clientMessage
        setClientMessageEvent e rw xmonad_restart 32 0 currentTime
        sendEvent dpy rw False structureNotifyMask e
    sync dpy False

I'm not familiar with the X11 API yet, but what weirds me out is the internAtom function and what the purpose of XMONAD_RESTART serves in the client message. There's also this handler:

handle e@ClientMessageEvent { ev_message_type = mt } = do
    a <- getAtom "XMONAD_RESTART"
    if (mt == a)
        then restart "xmonad" True
        else broadcastMessage e

handle e = broadcastMessage e -- trace (eventName e) -- ignoring

So I'm guessing atoms are just ad-hoc strings used to ID message events? I don't get any error messages when I run a restart so it would seem as if it makes it all the way to the operation handle:

restart :: String -> Bool -> X ()
restart prog resume = do
    broadcastMessage ReleaseResources
    io . flush =<< asks display
    let wsData = show . W.mapLayout show . windowset
        maybeShow (t, Right (PersistentExtension ext)) = Just (t, show ext)
        maybeShow (t, Left str) = Just (t, str)
        maybeShow _ = Nothing
        extState = return . show . catMaybes . map maybeShow . M.toList . extensibleState
    args <- if resume then gets (\s -> "--resume":wsData s:extState s) else return []
    catchIO (executeFile prog True args Nothing)

I'm suspecting there's a permission issue here that xmonad-x86_64-linux had back when it was compiled and launched by a system installation of xmonad rather than my sandboxed one (which is currently symlinked to the binary produced by stack).


Solution

  • It turns out the problem was that I couldn't use the xmonad binary produced by stack even though it was in my path and ~/.xmonad/xmonad-x86_64-linux was symlinked to it. Instead, I got restarting working by binding my mod-q to the following:

    restart "/home/oldmanmike/.xmonad/xmonad-x86_64-linux" True
    

    It will work provided I give it the absolute path.

    This makes some sense given an observation earlier that putting the following in my .xinitrc would cause X to crash:

    exec xmonad
    

    Instead, I had to use the following for the initial launch:

    exec ~/.xmonad/xmonad-x86_64-linux
    

    So for now, it would seem as if calling xmonad is a lot less reliable of a command and I should just alias the path to xmonad-x86_64-linux and call that from now on.