Thursday, December 11, 2014

Perfect Audio over RDP with padsp

Unfortunately, in the linux desktop world, some things just don't work.  Especially when it comes to webinars, web conferencing and the like, where the focus is on Windows desktops.  Using RDP to a virtual or physical Windows box is a good solution, except when it comes to audio.  Audio jitter/stutter is almost guaranteed on an RDP session.  Googling the topic, you'll even find several "experts" saying its just not possible to have quality audio over RDP.

The linux RDP client "rdesktop" was historically built against OSS, and under Ubuntu it was patched to work with ASLA, as OSS is kinda considered "old and non-standard".  So, yay, Ubuntu's rdesktop works with "-r sound:local" to get sound over an RDP session.. but again.. jittery - even over gigabit LAN.

Virtualbox comes with a custom RDP client, rdesktop-vrdp, which was tweaked for virtual rdp sessions, but still uses OSS, and therefore there is NO sound on RDP sessions (unless of course you build OSS support on your system - just not there in Ubuntu).

Alas!  The PulseAudio developers built this wrapper "padsp", which "starts  the  specified program and redirects its access to OSS compatible audio devices (/dev/dsp and auxiliary devices) to a PulseAudio sound server."  The result of running rdesktop-vrdp via this wrapper is just amazing.. audio is just PERFECT!  Absolutely no jitter in the audio at all.  Not only on a local network, but even across WAN.

Its so simple to launch too:  "padsp rdesktop-vrdp -r sound:local .....(whatever other params)"

padsp is part of pulseaudio-utils in Ubuntu

I have no idea why this wrapper works so good.. but kudos to the developers!  Or maybe its just OSS that is so efficient with networked audio??  If anyone knows, let us know in comments below.

Tuesday, October 21, 2014

Improving LTSP scripting facility

One thing that has been a pain with LTSP is that the scripts in /usr/share/ltsp/screen.d which control what launches on the thin client are all contained within the build image.  Any change/customization to those scripts then requires a rebuild/redeployment of the image.

I thought why not just use the tftp client to pull the script from the TFTP server instead.  Turns out this is quite easy to do.  I added the following script to the screen.d directory, and called it "tftp" (which was just based on one of the existing init scripts):


#!/bin/sh
#

#full path to file (excluding /var/lib/tftpboot)
filepath=$1
file=`basename $filepath`

if [ -n "$SERVER" ]; then
    script_temp=$(mktemp)
    tftp "$SERVER" -c get ${filepath} $script_temp
    # only execute if it has non-zero size.
    if [ -s "$script_temp" ]; then
        mv "$script_temp" /usr/share/ltsp/screen.d/${file}
        chmod 777 /usr/share/ltsp/screen.d/${file}
        exec /bin/bash /usr/share/ltsp/screen.d/${file}
    else
        rm "$script_temp"
        sleep 15
        exec /bin/bash --login
    fi
fi
Now in lts.conf I can do somthing like:

SCREEN_01 = tftp /ltsp-trusty/amd64/firefox

Now I have built the "/var/lib/tftpboot/ltsp-trusty/amd64/firefox" script on the TFTP server and it can easily/quickly customized as needed.

Here is an example (requires "apt-get install firefox openbox" in the ltsp-chroot image):

#!/bin/sh

. /usr/share/ltsp/screen-x-common

export HOME="${HOME:-/root}" USER="${USER:-root}"
COMMAND="openbox-session"
mkdir -p /root/.config/openbox
echo "/usr/bin/firefox http://somesite.biz" >> /root/.config/openbox/autostart
chmod 755 /root/.config/openbox/autostart

# The following logic is described at the top of xinitrc.
if [ -x /usr/share/ltsp/xinitrc ]; then
    xinit /usr/share/ltsp/xinitrc "$COMMAND" -- "$DISPLAY" "vt${TTY}" $X_ARGS >/dev/null
else
    eval "xinit $COMMAND -- $DISPLAY vt${TTY} $X_ARGS >/dev/null"
fi

if [ $? -ne 0 ]; then
    echo "Xserver failed, falling back to a text shell" >&2
    exec /bin/bash --login
fi

So simple things like having one thin-client launch firefox and another launch NX player, is easy thru lts.conf and script manipulation - no rebuilding of images required (apart from package installation of course).


Ubuntu LTSP for the Enterprise


Ubuntu LTSP (ltsp-server package) provides a very easy and convenient method for deploying a thin-client environment (see ltsp.org), but is centered primarily around one server being the DHCP/TFTP/NBD and image build server.  Being in an enterprise environment, I needed to be able to split these functions in order to deploy thin clients.  The image below depicts what I needed to achieve. Essentially I wanted to have ubuntu 14.04 images served by older release servers, so my "build" server needs to be 14.04.  Plus I wanted to be able to server multiple custom images off the same server(s).  Note:  I have existing DHCP/TFTP and NBD servers - so this post assumes these are already established - If you do not, then the ltsp-server package has everything you need, and this post is kinda moot.


First, on my build server, I did the following:

apt-get install ltsp-server
ltsp-build-client --dist trusty --arch amd64 --base /opt/ltsp-trusty

This builds your base client system, in my case an amd64 architecture ubuntu 14.04 base.  It takes about 20 minutes depending on your Internet connection and system speed.  Then any customizations can be done via ltsp-chroot:

ltsp-chroot -a amd64 -b /opt/ltsp-trusty --mount-package-cache

(more on what I customized in later post)

And to build the image:

ltsp-update-image --base /opt/ltsp-trusty

So what we have now on the build server, is the image in /opt/ltsp-trusty/images called amd64.img and the needed tftpboot files in /var/lib/tftpboot/ltsp.  In very simple terms, we now need to deploy the image "amd64.img" to the NBD server, and the PXE boot environment to the TFTP server, and then tweak configuration files to point to the right places.

NBD server (Network Block Device)

For every different image you want to deploy, you need nbd-server listening on a different port.  I am using inetd to spawn the nbd-server instances.  Here is my inetd.conf, which is configured for 3 images on ports 2000 thru 2002:


2000  stream  tcp nowait  nobody /usr/sbin/tcpd /usr/sbin/nbdrootd /opt/ltsp/images/i386.img
2001  stream  tcp nowait  nobody /usr/sbin/tcpd /usr/sbin/nbdrootd /opt/ltsp-trusty/images/amd64.img
2002  stream  tcp nowait  nobody /usr/sbin/tcpd /usr/sbin/nbdrootd /opt/ltsp-trusty/images/i386.img

The image files have to be accessible directly, so in my case I copied them from my build server.


TFTP server (Trivial File Transfer Protocol)

For every different image, you need a specific PXE environment defined in /var/lib/tftpboot.  In my case I have 3:

/var/lib/tftpboot/ltsp/i386
/var/lib/tftpboot/ltsp-trusty/amd64
/var/lib/tftpboot/ltsp-trusty/i386

These can be copied as is from the same location on the build server - each environment can have its own lts.conf file.

Now, there is some tweaking required in order for the client to grab the correct NBD image.  You need to add nbdroot={ipaddress}:{port} to the kernel boot parameters for any image served by the non-standard 2000 NBD port.  This is done in the pxelinux.cfg/ltsp file - snippet below.


# This file is regenerated when update-kernels runs.
# Do not edit, see /etc/ltsp/update-kernels.conf instead.
default ltsp-NBD
ontimeout ltsp-NBD

# This file is regenerated when update-kernels runs.
# Do not edit, see /etc/ltsp/update-kernels.conf instead.
label ltsp-NBD
menu label LTSP, using NBD
kernel vmlinuz-3.13.0-37-generic
#append ro initrd=initrd.img-3.13.0-37-generic init=/sbin/init-ltsp quiet splash root=/dev/nbd0
append ro initrd=initrd.img-3.13.0-37-generic init=/sbin/init-ltsp nosplash root=/dev/nbd0 nbdroot=192.168.202.4:2001
ipappend 2
DHCP server (Dynamic Host Configuration Protocol)

Your DHCP configuration is key, as it directs the PXE booting client to the correct TFTP server and PXE path.  I use an Etherboot gPXE image served by a local TFTP server to fix issues with older network cards - but you may not need that.  Here is one client config section example:

host webc2 {     hardware ethernet 20:cf:30:6f:0a:9e;
     fixed-address webc2;
     option host-name "webc2";
     if substring (option vendor-class-identifier, 0, 9) = "PXEClient"           {
   if exists user-class and option user-class = "gPXE" {
      next-server 192.168.202.4;
      filename "/ltsp-trusty/i386/pxelinux.0";
   } else { filename "/tftpboot/undionly.kpxe"; }
} else {
  next-server 192.168.202.4;
  filename "/ltsp-trusty/i386/nbi.img";}}

The "next-server" portion points it to the TFTP server and PXE file.

You'll probably notice that in my case, my TFTP server and NBD server are on the same server, but its not necessary.

Anyway, in another post I'll discuss an improvement I made to LTSP to allow better scripting via TFTP.

Thursday, June 5, 2014

Inserting/Pasting HTML into OpenOffice Calc via Basic macro

Its easy to manually copy data from a browser window and paste into an existing OO Calc doc.  If tables are used in the html, they paste nicely into rows and columns.

I needed to automate this process via a macro, but it wasn't as simple as I at first thought.

Its easy to use a filter with loadComponentFromURL, to create a NEW Calc doc.  But I wanted to place it specifically into an existing doc.

I then found ability to link to External Data via macro using AreaLinks.insertAtPosition.  This worked wonderfully, but I didn't like that it retained links.. I just wanted a dump of the data, no strings attached.

I moved to try doing copy and paste via a Shell command, wget | xclip, and .uno:Paste , but the paste was always "unformatted text" and thus pasted raw html.

I went back to AreaLinks.insertAtPosition .. figured if I can insert a link, and then remove the link (retaining the data), I've essentially achieved my objective.  So this is the macro I ended up with (works with OO 3.2 and LO 4.1):

Sub InsertWebLinkAtSelection(url As String, section As String)
   Dim oDoc As Object
   Dim oSelection
   Dim oAddress As new com.sun.star.table.CellAddress
      oDoc = ThisComponent
   oSelection = oDoc.getCurrentController().getSelection()
   if oSelection.supportsService("com.sun.star.sheet.SheetCell") then
      oAddress = oSelection.getCellAddress()
      oDoc.AreaLinks.insertAtPosition(oAddress,url,"HTML_" +
section,"calc_HTML_WebQuery","0 0")
      oDoc.AreaLinks.removeByIndex(getCount())
   endif
End Sub
The key was that last line... oDoc.AreaLinks.removeByIndex(getCount())  .. doesn't affect any exist links in document.  So it creates a link, which inserts the data at current cell address, and then removes the link.

Note:  The "section" argument is either "all" or "1", "2", "3",.. etc representing which html table element you want.

Note 2:  I also found that this method doesn't respect the http://user@pw:URL method, so I did have to use : Shell ("bash",0,"-c 'wget -O /tmp/file.html http://user@pw:URL....'", true) to get the results to a temp file, and then used InsertWebLinkAtSelection("file:///tmp/file.html","all")