XMonad and xmobar on OpenSolaris with functional monitoring (mutt to boot)

I’ve been having carpal tunnel flareups lately, so I went looking around for things I could do to use the mouse less and the keyboard more (as counter-intuitive as that may sound, I find that holding a mouse for hours irritates it far more than any amount of typing, YMMV). Vimperator is an obvious first step, but, well, I love vi, so I already had that running.

Tiling window managers came to mind. I’ve already used wmii and dwm once upon a time, but they’re hardly state of the art (as state of the art as tiling window managers get, anyway), and hacking together a reasonable workspace status bar in dzen/Perl didn’t appeal to me. Awesome3 (the window manager) does appeal to me, but getting it running on Solaris looked like a little more work than I wanted to invest, and I’m sick of working with moving targets (Awesome3 looks like they’re almost as “break your config” happy as Rails).

Mind you, I still love Openbox, but given that 99% of my time is spent in terminals (irssi, mutt, ssh, vim, mcabber, slash’em), I see no reason why I should even bother with having window decorations and manually arranging them at all.

Had I known what I was getting into, I probably would have just used Awesome. I mean, it needed two libraries I didn’t have, and some dzen hacking. Not… this. Not that I’m unhappy with XMonad, but…

Firstly, there’s no build of GHC (Glaskow Haskell Compiler) in the OpenSolaris repositories. There’s a pre-compiled version of GHC 6.10, but only for SPARC. Pre-compiled version of GHC 6.8 for x86/amd64, but that ain’t helping me (a scary amount of stuff from Hackage, Haskell’s version of CPAN/rubygems/whathaveyou doesn’t want to run in GHC 6.8, and the recommended fix for some bugs is “upgrade to 6.10″).

No GCC 4 in the repos either, and GCC 4.1.2 is the recommended version for building GHC. So, onto the magic. Don’t even try with SunStudio. GNU-isms in the code stopped me dead.

I was bitching to Dan about how ridiculous this process was a few weeks ago. Maybe it’ll help somebody.

Firstly, either install readline from the OpenSolaris Pending repositories or compile it yourself.

Next, we need to bootstrap gcc. For that, we’ll need gmp from GNU and mpfr. Grab the precompiled version of GHC 6.8.2 while you’re at it. You should also get the newest versions of ghc and ghc-$version and ghc-$version-src-extralibs to get running later.

Complaint #1: GNU automake is braindead. It, I assume, just checks `uname` and not `isainfo`, so I can’t tell when we’re running 64 bit. Either use Solaris libtools or do the following for gmp and MPFR

./configure ABI=32 && gmake && pfexec gmake install
make distclean
configure --prefix=/usr/local/lib64 && gmake && pfexec gmake install

If you don’t specify another prefix, it’ll stomp all over the 32 bit libraries it just installed on your 64 bit box.

Complain #2: gcc is even more braindead. It’ll build a 64 bit binary but link it against 32 bit libraries, then eat itself during stage 2 bootstrap. You’d think the FSF would be smarter, but no. It just finds the wrong ELFCLASS down the line. To correct:

export LDFLAGS=-L/opt/local/lib -L/opt/local/lib/64 \
-R/usr/local/lib:/usr/local/lib/64
export LD_OPTIONS=-L/opt/local/lib -L/opt/local/lib/64 \
-R/usr/local/lib:/usr/local/lib/64
./configure && gmake -j4 && pfexec gmake install

Complaint #3: The precompiled ghc-6.8.2 we got? It sucks. The rts library is broken (check it with ldd). It would be nice to avoid this, but, well… Bootstrapping ghc from C sources and no Haskell compiler involves another goddamn system which DOES have Haskell installed, AND knowledge of what registers your CPU uses. Whoever thought up that notion of bootstrapping? Well… The “goal” is to have Haskell self-bootstrap (it currently does not, since, ironically, Haskell is too “pure” to actually be written in a “pure” language, since we need dirty things like actually doing something useful with “impure” data, like user input or stuff sucked in from a file).

cd ../ghc-6.8.2
./configure && pfexec gmake install
ghc-pkg describe rts > rts.pkg
vim rts.pkg
#add -R/usr/local/lib to the end of the ldoptions field, or ghc bombs bootstrapping the new version in stage 2
ghc-pkg update rts.pkg

Complaint #4: GHC is even stupider than GCC, if possible. Not only do we have to prepend /usr/local/bin to $PATH so GHC can find our shiny new gcc-4.1.2, we have to pass ridiculous amounts of config flags (including one which tells it where GCC is — if the $PATH OR –with-gcc is wrong, it won’t bootstrap. Don’t ask, because I don’t know why).

export PATH=/usr/local/bin:$PATH
./configure --with-gcc=/usr/local/bin/gcc --with-gmp-libraries=/usr/local/lib --with-gmp-include=/usr/local/include --with-readline-libraries=/usr/local/bin --with-readline-include=/usr/local/include
gmake -j4 && pfexec gmake install

Yay! Working GHC. Sadly, if you want to reclaim the 350MB or so the GHC 6.8 install is taking up, you have to go remove it yourself (apparently the GHC team does not believe in `make uninstall`). This means we can install cabal, which requires nothing special, other than you grabbing the tarball and installing it as normal.

Next, `cabal install xmonad xmonad-contrib` I said we were going to install xmobar, and we are, but it’s a little tricker. You see, even though Xmobar mostly reads a pipe to give us a title and workspace listing, the plugins are not optional. They also depend on libnotify, which is only present on Linux. Good job, xmobar developer! Fortunately, this is easily corrected, and xmobar (mostly) works. Caveats explained later.

You can’t just `cabal install` xmobar. No-go since hinotify will not install, and there’s not a clear explanation as to why from the output. As noted, it doesn’t really depend on it, just that the developer can’t be bothered to use Haskell’s typing system to throw messages at you when you try to use features that are not implemented. So… edit ~/.cabal/packages/hackage.haskell.org/xmobar/$version/xmobar-$version/xmonad.cabal

Take out the lines referring to hinotify. Then `cabal build && cabal install` from the directory xmonad.cabal was in. Ooh and aah, but don’t try to use, well… anything. BatteryReader, CpuReader, MemReader, Net, Swap, all broken. Thankfully, we have Dtrace and Python to replace it with, since xmobar’s PipeReader still works.

Memory usage?

#pragma D option quiet
#pragma D option bufsize=16k
 
dtrace:::BEGIN
{
}
 
profile:::tick-1sec
{
	/* RAM stats */
	this->ram_total = `physinstalled;
	this->unusable  = `physinstalled - `physmem;
	this->locked    = `pages_locked;
	this->ram_used  = `availrmem - `freemem;
	this->freemem   = `freemem;
	this->kernel    = `physmem - `pages_locked - `availrmem;
 
	this->ram_total	*= `_pagesize;  this->ram_total	/= 1048576;
	this->unusable	*= `_pagesize;  this->unusable	/= 1048576;
	this->kernel	*= `_pagesize;  this->kernel	/= 1048576;
	this->locked	*= `_pagesize;  this->locked	/= 1048576;
	this->ram_used	*= `_pagesize;  this->ram_used	/= 1048576;
	this->freemem	*= `_pagesize;  this->freemem	/= 1048576;
	printf("RAM: %2d%%\n", ((this->ram_total - this->freemem) * 100 / this->ram_total));
}

Network speeds?

#!/usr/sbin/dtrace -s
#pragma D option quiet
dtrace:::BEGIN
{
	TCP_out = 0; TCP_in = 0;
}
 
 
mib:::tcpOutDataBytes		{ TCP_out += arg0;   }
mib:::tcpInDataInorderBytes	{ TCP_in += arg0;    }
 
profile:::tick-1sec
{
	OUT_print = TCP_out/1024; IN_print = TCP_in/1024;
	printf("Out:%3d|In:%3d", OUT_print, IN_print);
	TCP_out = 0;
	TCP_in = 0;
 
}

.xmobarrc

Config { font = "-*-terminus-*-*-*-*-12-*-*-*-*-*-*-u"
       , bgColor = "#000000"
       , fgColor = "#AFAF87"
       , position = Top 
       , lowerOnStart = True
       , commands = [ Run Date "%a %b %_d %Y %H:%M:%S" "date" 10 
                    , Run Weather "KSTP" ["-t","<tempF>F","-L","64","-H","77","--normal","green","--high","red","--low","lightblue"] 36000
		    , Run PipeReader "/export/home/ryan/dtrace/net" "wireless"
		    , Run PipeReader "/export/home/ryan/dtrace/netspeed" "speed"
		    , Run PipeReader "/export/home/ryan/dtrace/psr" "cpui"
		    , Run PipeReader "/export/home/ryan/dtrace/ram" "mem"
                    , Run StdinReader
                    ]
       , sepChar = "%"
       , alignSep = "}{"
       , template = " %StdinReader% } { %cpui% | %mem% | %wireless% %speed% | %date% | %KSTP%"
       }

A script to feed those pipes. If you don’t have python2.6, pexpect on python2.4 (the Solaris/OpenSolaris default) works. Just install pexpect with easy_install, an .egg, or whatever your poison may be.

#!/usr/bin/python2.6
import math
import os
import platform
import re
import stat
import sys
import time
 
import pexpect
 
#Get the directory we're running from to create the fifos rather than the $pwd of whatever called us
path = os.path.dirname( os.path.realpath(__file__)) + "/"
 
wificonfig = ""
 
#Y'know, I haven't actually written the iwconfig thing.  It's here for posterity and possible later use.
osystem = platform.system()
if osystem == 'SunOS':
  wificonfig = 'wificonfig'
elif osystem == 'Linux':
  wificonfig = 'iwconfig'
 
 
#Kill off any old instances which may be running.  Poor man's pkill, but guaranteed to work pretty much anywhere.
pexpect.run('bash -c "ps -ef |grep mpstat |grep -v python| awk \'{print $2}\' | xargs kill -9"')
pexpect.run('bash -c "ps -ef | grep speed.d | awk \'{print $2}\' | xargs kill -9')
pexpect.run('bash -c "ps -ef | grep meminfo.d | awk \'{print $2}\' | xargs kill -9')
 
def checkfifo(path):
  #If it ain't there, make it
  if not os.path.exists(path):
    os.mkfifo(path)
    handle = open(path, "r+")
    return handle
  #If it is, just return it
  elif stat.S_ISFIFO(os.stat(path).st_mode):
    handle = open(path, "r+")
    return handle
  else:
    if os.path.isfile(path):
      #Not a FIFO, and it needs to be
      os.unlink(path)
    os.mkfifo(path)
    handle = open(path, "r+")
    return handle
 
#Set up our fifos
psrfifo = checkfifo(path + "psr")
netfifo = checkfifo(path + "net")
nspdfifo = checkfifo(path + "netspeed")
ramfifo = checkfifo(path + "ram")
 
#Fire off the processes we'll be reading from.  Using pexpect seems like overkill, but mpstat is apparently smart enough to tell when it's being read from a pipe, and it'll buffer no matter what you do.  pexpect/expect fake being interactive, so it happily runs without buffering.
mpstat = pexpect.spawn('bash -c "mpstat 1 | grep -v CPU"')
ramstats = pexpect.spawn(path + "meminfo.d")
nspeed = pexpect.spawn(path + "speed.d")
 
#Regular expressions to use later.  Since it's a long-runnign script, they may as well be compiled
mpre = re.compile(r'^\s+?(?P<cpu>\d+).*?(?P<idle>\d+)$')
solwifire = re.compile(r'.*?linkstatus: (?P<status>\w+).*essid: (?P<essid>\w+).*strength: \w+\((?P<strength>\d+)\).*', re.DOTALL)
 
def prstat():
  #Yay for awk/sed abuse, but it's concise and I'm already forking.  Basically getting a list of CPUs to check later, so this script should perform its duty no matter if you have 1 CPU or 128 (T2 users)
  psrinfo = pexpect.run('bash -c "psrinfo -v |grep MHz | awk \'{print $6,$7}\' | sed -e \'s/,//\'"').rstrip().split('\r\n')
  output = ""
  for cpu in psrinfo:
    line = prmatch(cpu)
    output = output + line
  psrfifo.write(output + "\n")
  psrfifo.flush()
 
def prmatch(cpu):
  line = mpstat.readline().rstrip()
  m = mpre.match(line)
  #Match it against our earlier regex and subtract the idle value from 100 to get the actual used percentage, which isn't wholly accurate (IOWAIT and whatnot), but it's good enough for me
  usage = 100 - int(m.group('idle'))
  #Padding the string seems stupid, and it is, but xmobar arbitrarily decides spots that it's not going to refresh even if text shows up there, leaving it (black in my case) when text slides.  Padding fixes that.  Also, if your CPU goes to 100%, you probably shouldn't have a script which reads dtrace probes running.  Just sayin'.
  return "Cpu%s: %2d%% (%s) " % (m.group('cpu'), usage, cpu)
 
def memory():
  #I haven't found any swap information from dtrace probes as easy to manipulate as thi sis
  swap = pexpect.run('bash -c "/usr/sbin/swap -l |tail -n 1 | awk \'{print $4, $5}\'"').rstrip().split(' ')
  #Ugly?  You bet.  Cast the subtract free swap from total swap blocks, divide it by free swap blocks * 100 cast to an int to give us an actual percentage, floor that, then cast THAT to an int
  usedswap = int(math.floor(((int(swap[1])-int(swap[0]))/int(swap[1])*100)))
  ram = ramstats.readline().rstrip()
  output =  "%s Swap: %2d%%" % (ram, usedswap)
  ramfifo.write(output + "\n")
  ramfifo.flush()
 
def network():
  #Filter out interface which aren't up, which are vnics, which only point to localhost to get the running interface.  I'm assuming you only have one at a time, but if you have more modify this to suit.
  iface = pexpect.run('bash -c "ifconfig -a | grep UP |grep RUNNING| grep -v IPv6 |grep -v lo | grep -v -E \':[0-9]: \' | awk \'{print $1}\' | sed -e \'s/://\'i"').rstrip()
  if osystem == 'SunOS':
    command = "wificonfig -i " + iface + " showstatus"
    status = pexpect.run(command).rstrip()
    output = ""
    if solwifire.match(status):
      #Beauty of regexes.  If it doesn't match, it's not wireless (or not connected).  It if is, give us values.
      m = solwifire.match(status)
      strength = math.floor((int(m.group('strength')) / 15.) * 100)
      output =  "%s: %s(%s) %3d%%" % (iface, m.group('status'), m.group('essid'), strength)
    else:
      #Probably not wireless
      output = iface + ":"
    netfifo.write(output + "\n") 
    netfifo.flush()
 
def netspeed():
  #Is this method really necessary?  Couldn't the dtrace probe just write to the fifo itself?  Probably, but if you (or I) want to colorize it at some point, it may as well get sucked in here.
  speed = nspeed.readline()
  nspdfifo.write(speed + "\n")
  nspdfifo.flush()
 
while 1:
 
  prstat()
  memory()
  network()
  netspeed()
  time.sleep(1)

It’s not the prettiest python. I should probably move those repetitive fifo flushes/etc to a method, but I didn’t honestly expect that I’d need to replace this much XMobar functionality. Notably, XMobar can colorize things with <span> attributes setting colors in case somebody wanted to pretty up the usages (really, to make it look more like XMobar’s [colorized] defaults for CPU/net usage). I don’t care, personally. I didn’t implement a battery monitor either, but hey, you can if you want to.

Complaint #5 (did I lose track?): SUNWmutt doesn’t have support for header caching, which is a real bitch when I have 12,000 emails. It also doesn’t support SMTP over SSL, making it pretty well worthless for Gmail. I have other accounts I use mutt for, but Gmail’s an important one.

This isn’t that tough, really. You need some kind of a database for the mutt config script, and gdbm is trivially easy to get running (normal ./configure && make && make install). On the other hand, we run into two hiccups. The configure file depends on ncurses, which is just a link to plain ol’ curses on lots of Solaris boxes. Secondly, (and I don’t really begrudge the Mutt guys for this, since there’s actually a commit to fix this, unlike GCC and GHC, whose response is “too fucking bad” [GHC actually posts the recommendation for fixing rts.pkg and the configure flags on their own site rather than FIXING THE BUILD]), configure.ac does some things wrong with libidn.

Find $with_idn, and replace the block which follows it with this (–with-idn doesn’t seem to build properly).:

if test "$with_idn" != "no" ; then
  if test "$with_idn" != "yes" ; then
    AC_CHECK_HEADERS([idn/idn-int.h],
      [AC_CHECK_HEADERS([idn/idna.h], [],
        [CPPFLAGS="$CPPFLAGS -I/usr/include/idn"])])
  fi
fi

If you don’t have or want ncurses (or it’s a symlink on your system), fix configure. `sed -i -e ’s/-lncurses/-lcurses/’ configure`.

./configure –with-regex –with-gnutls –enable-hcache –enable-smtp –enable-imap –enable-pop –enable-mailtool –with-sasl –with-idn=/usr/include/idn

Congratulations!

Next up, re-implementing htop for Solaris with dtrace probes, python, and ncurses.

No Comments

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a comment

You must be logged in to post a comment.